index.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.PathError = exports.TokenData = void 0;
  4. exports.parse = parse;
  5. exports.compile = compile;
  6. exports.match = match;
  7. exports.pathToRegexp = pathToRegexp;
  8. exports.stringify = stringify;
  9. const DEFAULT_DELIMITER = "/";
  10. const NOOP_VALUE = (value) => value;
  11. const ID_START = /^[$_\p{ID_Start}]$/u;
  12. const ID_CONTINUE = /^[$\u200c\u200d\p{ID_Continue}]$/u;
  13. const SIMPLE_TOKENS = {
  14. // Groups.
  15. "{": "{",
  16. "}": "}",
  17. // Reserved.
  18. "(": "(",
  19. ")": ")",
  20. "[": "[",
  21. "]": "]",
  22. "+": "+",
  23. "?": "?",
  24. "!": "!",
  25. };
  26. /**
  27. * Escape text for stringify to path.
  28. */
  29. function escapeText(str) {
  30. return str.replace(/[{}()\[\]+?!:*\\]/g, "\\$&");
  31. }
  32. /**
  33. * Escape a regular expression string.
  34. */
  35. function escape(str) {
  36. return str.replace(/[.+*?^${}()[\]|/\\]/g, "\\$&");
  37. }
  38. /**
  39. * Tokenized path instance.
  40. */
  41. class TokenData {
  42. constructor(tokens, originalPath) {
  43. this.tokens = tokens;
  44. this.originalPath = originalPath;
  45. }
  46. }
  47. exports.TokenData = TokenData;
  48. /**
  49. * ParseError is thrown when there is an error processing the path.
  50. */
  51. class PathError extends TypeError {
  52. constructor(message, originalPath) {
  53. let text = message;
  54. if (originalPath)
  55. text += `: ${originalPath}`;
  56. text += `; visit https://git.new/pathToRegexpError for info`;
  57. super(text);
  58. this.originalPath = originalPath;
  59. }
  60. }
  61. exports.PathError = PathError;
  62. /**
  63. * Parse a string for the raw tokens.
  64. */
  65. function parse(str, options = {}) {
  66. const { encodePath = NOOP_VALUE } = options;
  67. const chars = [...str];
  68. const tokens = [];
  69. let index = 0;
  70. let pos = 0;
  71. function name() {
  72. let value = "";
  73. if (ID_START.test(chars[index])) {
  74. do {
  75. value += chars[index++];
  76. } while (ID_CONTINUE.test(chars[index]));
  77. }
  78. else if (chars[index] === '"') {
  79. let quoteStart = index;
  80. while (index++ < chars.length) {
  81. if (chars[index] === '"') {
  82. index++;
  83. quoteStart = 0;
  84. break;
  85. }
  86. // Increment over escape characters.
  87. if (chars[index] === "\\")
  88. index++;
  89. value += chars[index];
  90. }
  91. if (quoteStart) {
  92. throw new PathError(`Unterminated quote at index ${quoteStart}`, str);
  93. }
  94. }
  95. if (!value) {
  96. throw new PathError(`Missing parameter name at index ${index}`, str);
  97. }
  98. return value;
  99. }
  100. while (index < chars.length) {
  101. const value = chars[index];
  102. const type = SIMPLE_TOKENS[value];
  103. if (type) {
  104. tokens.push({ type, index: index++, value });
  105. }
  106. else if (value === "\\") {
  107. tokens.push({ type: "escape", index: index++, value: chars[index++] });
  108. }
  109. else if (value === ":") {
  110. tokens.push({ type: "param", index: index++, value: name() });
  111. }
  112. else if (value === "*") {
  113. tokens.push({ type: "wildcard", index: index++, value: name() });
  114. }
  115. else {
  116. tokens.push({ type: "char", index: index++, value });
  117. }
  118. }
  119. tokens.push({ type: "end", index, value: "" });
  120. function consumeUntil(endType) {
  121. const output = [];
  122. while (true) {
  123. const token = tokens[pos++];
  124. if (token.type === endType)
  125. break;
  126. if (token.type === "char" || token.type === "escape") {
  127. let path = token.value;
  128. let cur = tokens[pos];
  129. while (cur.type === "char" || cur.type === "escape") {
  130. path += cur.value;
  131. cur = tokens[++pos];
  132. }
  133. output.push({
  134. type: "text",
  135. value: encodePath(path),
  136. });
  137. continue;
  138. }
  139. if (token.type === "param" || token.type === "wildcard") {
  140. output.push({
  141. type: token.type,
  142. name: token.value,
  143. });
  144. continue;
  145. }
  146. if (token.type === "{") {
  147. output.push({
  148. type: "group",
  149. tokens: consumeUntil("}"),
  150. });
  151. continue;
  152. }
  153. throw new PathError(`Unexpected ${token.type} at index ${token.index}, expected ${endType}`, str);
  154. }
  155. return output;
  156. }
  157. return new TokenData(consumeUntil("end"), str);
  158. }
  159. /**
  160. * Compile a string to a template function for the path.
  161. */
  162. function compile(path, options = {}) {
  163. const { encode = encodeURIComponent, delimiter = DEFAULT_DELIMITER } = options;
  164. const data = typeof path === "object" ? path : parse(path, options);
  165. const fn = tokensToFunction(data.tokens, delimiter, encode);
  166. return function path(params = {}) {
  167. const [path, ...missing] = fn(params);
  168. if (missing.length) {
  169. throw new TypeError(`Missing parameters: ${missing.join(", ")}`);
  170. }
  171. return path;
  172. };
  173. }
  174. function tokensToFunction(tokens, delimiter, encode) {
  175. const encoders = tokens.map((token) => tokenToFunction(token, delimiter, encode));
  176. return (data) => {
  177. const result = [""];
  178. for (const encoder of encoders) {
  179. const [value, ...extras] = encoder(data);
  180. result[0] += value;
  181. result.push(...extras);
  182. }
  183. return result;
  184. };
  185. }
  186. /**
  187. * Convert a single token into a path building function.
  188. */
  189. function tokenToFunction(token, delimiter, encode) {
  190. if (token.type === "text")
  191. return () => [token.value];
  192. if (token.type === "group") {
  193. const fn = tokensToFunction(token.tokens, delimiter, encode);
  194. return (data) => {
  195. const [value, ...missing] = fn(data);
  196. if (!missing.length)
  197. return [value];
  198. return [""];
  199. };
  200. }
  201. const encodeValue = encode || NOOP_VALUE;
  202. if (token.type === "wildcard" && encode !== false) {
  203. return (data) => {
  204. const value = data[token.name];
  205. if (value == null)
  206. return ["", token.name];
  207. if (!Array.isArray(value) || value.length === 0) {
  208. throw new TypeError(`Expected "${token.name}" to be a non-empty array`);
  209. }
  210. return [
  211. value
  212. .map((value, index) => {
  213. if (typeof value !== "string") {
  214. throw new TypeError(`Expected "${token.name}/${index}" to be a string`);
  215. }
  216. return encodeValue(value);
  217. })
  218. .join(delimiter),
  219. ];
  220. };
  221. }
  222. return (data) => {
  223. const value = data[token.name];
  224. if (value == null)
  225. return ["", token.name];
  226. if (typeof value !== "string") {
  227. throw new TypeError(`Expected "${token.name}" to be a string`);
  228. }
  229. return [encodeValue(value)];
  230. };
  231. }
  232. /**
  233. * Transform a path into a match function.
  234. */
  235. function match(path, options = {}) {
  236. const { decode = decodeURIComponent, delimiter = DEFAULT_DELIMITER } = options;
  237. const { regexp, keys } = pathToRegexp(path, options);
  238. const decoders = keys.map((key) => {
  239. if (decode === false)
  240. return NOOP_VALUE;
  241. if (key.type === "param")
  242. return decode;
  243. return (value) => value.split(delimiter).map(decode);
  244. });
  245. return function match(input) {
  246. const m = regexp.exec(input);
  247. if (!m)
  248. return false;
  249. const path = m[0];
  250. const params = Object.create(null);
  251. for (let i = 1; i < m.length; i++) {
  252. if (m[i] === undefined)
  253. continue;
  254. const key = keys[i - 1];
  255. const decoder = decoders[i - 1];
  256. params[key.name] = decoder(m[i]);
  257. }
  258. return { path, params };
  259. };
  260. }
  261. function pathToRegexp(path, options = {}) {
  262. const { delimiter = DEFAULT_DELIMITER, end = true, sensitive = false, trailing = true, } = options;
  263. const keys = [];
  264. const flags = sensitive ? "" : "i";
  265. const sources = [];
  266. for (const input of pathsToArray(path, [])) {
  267. const data = typeof input === "object" ? input : parse(input, options);
  268. for (const tokens of flatten(data.tokens, 0, [])) {
  269. sources.push(toRegExpSource(tokens, delimiter, keys, data.originalPath));
  270. }
  271. }
  272. let pattern = `^(?:${sources.join("|")})`;
  273. if (trailing)
  274. pattern += `(?:${escape(delimiter)}$)?`;
  275. pattern += end ? "$" : `(?=${escape(delimiter)}|$)`;
  276. const regexp = new RegExp(pattern, flags);
  277. return { regexp, keys };
  278. }
  279. /**
  280. * Convert a path or array of paths into a flat array.
  281. */
  282. function pathsToArray(paths, init) {
  283. if (Array.isArray(paths)) {
  284. for (const p of paths)
  285. pathsToArray(p, init);
  286. }
  287. else {
  288. init.push(paths);
  289. }
  290. return init;
  291. }
  292. /**
  293. * Generate a flat list of sequence tokens from the given tokens.
  294. */
  295. function* flatten(tokens, index, init) {
  296. if (index === tokens.length) {
  297. return yield init;
  298. }
  299. const token = tokens[index];
  300. if (token.type === "group") {
  301. for (const seq of flatten(token.tokens, 0, init.slice())) {
  302. yield* flatten(tokens, index + 1, seq);
  303. }
  304. }
  305. else {
  306. init.push(token);
  307. }
  308. yield* flatten(tokens, index + 1, init);
  309. }
  310. /**
  311. * Transform a flat sequence of tokens into a regular expression.
  312. */
  313. function toRegExpSource(tokens, delimiter, keys, originalPath) {
  314. let result = "";
  315. let backtrack = "";
  316. let isSafeSegmentParam = true;
  317. for (const token of tokens) {
  318. if (token.type === "text") {
  319. result += escape(token.value);
  320. backtrack += token.value;
  321. isSafeSegmentParam || (isSafeSegmentParam = token.value.includes(delimiter));
  322. continue;
  323. }
  324. if (token.type === "param" || token.type === "wildcard") {
  325. if (!isSafeSegmentParam && !backtrack) {
  326. throw new PathError(`Missing text before "${token.name}" ${token.type}`, originalPath);
  327. }
  328. if (token.type === "param") {
  329. result += `(${negate(delimiter, isSafeSegmentParam ? "" : backtrack)}+)`;
  330. }
  331. else {
  332. result += `([\\s\\S]+)`;
  333. }
  334. keys.push(token);
  335. backtrack = "";
  336. isSafeSegmentParam = false;
  337. continue;
  338. }
  339. }
  340. return result;
  341. }
  342. /**
  343. * Block backtracking on previous text and ignore delimiter string.
  344. */
  345. function negate(delimiter, backtrack) {
  346. if (backtrack.length < 2) {
  347. if (delimiter.length < 2)
  348. return `[^${escape(delimiter + backtrack)}]`;
  349. return `(?:(?!${escape(delimiter)})[^${escape(backtrack)}])`;
  350. }
  351. if (delimiter.length < 2) {
  352. return `(?:(?!${escape(backtrack)})[^${escape(delimiter)}])`;
  353. }
  354. return `(?:(?!${escape(backtrack)}|${escape(delimiter)})[\\s\\S])`;
  355. }
  356. /**
  357. * Stringify an array of tokens into a path string.
  358. */
  359. function stringifyTokens(tokens) {
  360. let value = "";
  361. let i = 0;
  362. function name(value) {
  363. const isSafe = isNameSafe(value) && isNextNameSafe(tokens[i]);
  364. return isSafe ? value : JSON.stringify(value);
  365. }
  366. while (i < tokens.length) {
  367. const token = tokens[i++];
  368. if (token.type === "text") {
  369. value += escapeText(token.value);
  370. continue;
  371. }
  372. if (token.type === "group") {
  373. value += `{${stringifyTokens(token.tokens)}}`;
  374. continue;
  375. }
  376. if (token.type === "param") {
  377. value += `:${name(token.name)}`;
  378. continue;
  379. }
  380. if (token.type === "wildcard") {
  381. value += `*${name(token.name)}`;
  382. continue;
  383. }
  384. throw new TypeError(`Unknown token type: ${token.type}`);
  385. }
  386. return value;
  387. }
  388. /**
  389. * Stringify token data into a path string.
  390. */
  391. function stringify(data) {
  392. return stringifyTokens(data.tokens);
  393. }
  394. /**
  395. * Validate the parameter name contains valid ID characters.
  396. */
  397. function isNameSafe(name) {
  398. const [first, ...rest] = name;
  399. return ID_START.test(first) && rest.every((char) => ID_CONTINUE.test(char));
  400. }
  401. /**
  402. * Validate the next token does not interfere with the current param name.
  403. */
  404. function isNextNameSafe(token) {
  405. if (token && token.type === "text")
  406. return !ID_CONTINUE.test(token.value[0]);
  407. return true;
  408. }
  409. //# sourceMappingURL=index.js.map