index.js 14 KB

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