index.js 12 KB

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