403 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			403 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| "use strict";
 | |
| Object.defineProperty(exports, "__esModule", { value: true });
 | |
| exports.TokenData = void 0;
 | |
| exports.parse = parse;
 | |
| exports.compile = compile;
 | |
| exports.match = match;
 | |
| exports.pathToRegexp = pathToRegexp;
 | |
| exports.stringify = stringify;
 | |
| const DEFAULT_DELIMITER = "/";
 | |
| const NOOP_VALUE = (value) => value;
 | |
| const ID_START = /^[$_\p{ID_Start}]$/u;
 | |
| const ID_CONTINUE = /^[$\u200c\u200d\p{ID_Continue}]$/u;
 | |
| const DEBUG_URL = "https://git.new/pathToRegexpError";
 | |
| const SIMPLE_TOKENS = {
 | |
|     // Groups.
 | |
|     "{": "{",
 | |
|     "}": "}",
 | |
|     // Reserved.
 | |
|     "(": "(",
 | |
|     ")": ")",
 | |
|     "[": "[",
 | |
|     "]": "]",
 | |
|     "+": "+",
 | |
|     "?": "?",
 | |
|     "!": "!",
 | |
| };
 | |
| /**
 | |
|  * Escape text for stringify to path.
 | |
|  */
 | |
| function escapeText(str) {
 | |
|     return str.replace(/[{}()\[\]+?!:*]/g, "\\$&");
 | |
| }
 | |
| /**
 | |
|  * Escape a regular expression string.
 | |
|  */
 | |
| function escape(str) {
 | |
|     return str.replace(/[.+*?^${}()[\]|/\\]/g, "\\$&");
 | |
| }
 | |
| /**
 | |
|  * Tokenize input string.
 | |
|  */
 | |
| function* lexer(str) {
 | |
|     const chars = [...str];
 | |
|     let i = 0;
 | |
|     function name() {
 | |
|         let value = "";
 | |
|         if (ID_START.test(chars[++i])) {
 | |
|             value += chars[i];
 | |
|             while (ID_CONTINUE.test(chars[++i])) {
 | |
|                 value += chars[i];
 | |
|             }
 | |
|         }
 | |
|         else if (chars[i] === '"') {
 | |
|             let pos = i;
 | |
|             while (i < chars.length) {
 | |
|                 if (chars[++i] === '"') {
 | |
|                     i++;
 | |
|                     pos = 0;
 | |
|                     break;
 | |
|                 }
 | |
|                 if (chars[i] === "\\") {
 | |
|                     value += chars[++i];
 | |
|                 }
 | |
|                 else {
 | |
|                     value += chars[i];
 | |
|                 }
 | |
|             }
 | |
|             if (pos) {
 | |
|                 throw new TypeError(`Unterminated quote at ${pos}: ${DEBUG_URL}`);
 | |
|             }
 | |
|         }
 | |
|         if (!value) {
 | |
|             throw new TypeError(`Missing parameter name at ${i}: ${DEBUG_URL}`);
 | |
|         }
 | |
|         return value;
 | |
|     }
 | |
|     while (i < chars.length) {
 | |
|         const value = chars[i];
 | |
|         const type = SIMPLE_TOKENS[value];
 | |
|         if (type) {
 | |
|             yield { type, index: i++, value };
 | |
|         }
 | |
|         else if (value === "\\") {
 | |
|             yield { type: "ESCAPED", index: i++, value: chars[i++] };
 | |
|         }
 | |
|         else if (value === ":") {
 | |
|             const value = name();
 | |
|             yield { type: "PARAM", index: i, value };
 | |
|         }
 | |
|         else if (value === "*") {
 | |
|             const value = name();
 | |
|             yield { type: "WILDCARD", index: i, value };
 | |
|         }
 | |
|         else {
 | |
|             yield { type: "CHAR", index: i, value: chars[i++] };
 | |
|         }
 | |
|     }
 | |
|     return { type: "END", index: i, value: "" };
 | |
| }
 | |
| class Iter {
 | |
|     constructor(tokens) {
 | |
|         this.tokens = tokens;
 | |
|     }
 | |
|     peek() {
 | |
|         if (!this._peek) {
 | |
|             const next = this.tokens.next();
 | |
|             this._peek = next.value;
 | |
|         }
 | |
|         return this._peek;
 | |
|     }
 | |
|     tryConsume(type) {
 | |
|         const token = this.peek();
 | |
|         if (token.type !== type)
 | |
|             return;
 | |
|         this._peek = undefined; // Reset after consumed.
 | |
|         return token.value;
 | |
|     }
 | |
|     consume(type) {
 | |
|         const value = this.tryConsume(type);
 | |
|         if (value !== undefined)
 | |
|             return value;
 | |
|         const { type: nextType, index } = this.peek();
 | |
|         throw new TypeError(`Unexpected ${nextType} at ${index}, expected ${type}: ${DEBUG_URL}`);
 | |
|     }
 | |
|     text() {
 | |
|         let result = "";
 | |
|         let value;
 | |
|         while ((value = this.tryConsume("CHAR") || this.tryConsume("ESCAPED"))) {
 | |
|             result += value;
 | |
|         }
 | |
|         return result;
 | |
|     }
 | |
| }
 | |
| /**
 | |
|  * Tokenized path instance.
 | |
|  */
 | |
| class TokenData {
 | |
|     constructor(tokens) {
 | |
|         this.tokens = tokens;
 | |
|     }
 | |
| }
 | |
| exports.TokenData = TokenData;
 | |
| /**
 | |
|  * Parse a string for the raw tokens.
 | |
|  */
 | |
| function parse(str, options = {}) {
 | |
|     const { encodePath = NOOP_VALUE } = options;
 | |
|     const it = new Iter(lexer(str));
 | |
|     function consume(endType) {
 | |
|         const tokens = [];
 | |
|         while (true) {
 | |
|             const path = it.text();
 | |
|             if (path)
 | |
|                 tokens.push({ type: "text", value: encodePath(path) });
 | |
|             const param = it.tryConsume("PARAM");
 | |
|             if (param) {
 | |
|                 tokens.push({
 | |
|                     type: "param",
 | |
|                     name: param,
 | |
|                 });
 | |
|                 continue;
 | |
|             }
 | |
|             const wildcard = it.tryConsume("WILDCARD");
 | |
|             if (wildcard) {
 | |
|                 tokens.push({
 | |
|                     type: "wildcard",
 | |
|                     name: wildcard,
 | |
|                 });
 | |
|                 continue;
 | |
|             }
 | |
|             const open = it.tryConsume("{");
 | |
|             if (open) {
 | |
|                 tokens.push({
 | |
|                     type: "group",
 | |
|                     tokens: consume("}"),
 | |
|                 });
 | |
|                 continue;
 | |
|             }
 | |
|             it.consume(endType);
 | |
|             return tokens;
 | |
|         }
 | |
|     }
 | |
|     const tokens = consume("END");
 | |
|     return new TokenData(tokens);
 | |
| }
 | |
| /**
 | |
|  * Compile a string to a template function for the path.
 | |
|  */
 | |
| function compile(path, options = {}) {
 | |
|     const { encode = encodeURIComponent, delimiter = DEFAULT_DELIMITER } = options;
 | |
|     const data = path instanceof TokenData ? path : parse(path, options);
 | |
|     const fn = tokensToFunction(data.tokens, delimiter, encode);
 | |
|     return function path(data = {}) {
 | |
|         const [path, ...missing] = fn(data);
 | |
|         if (missing.length) {
 | |
|             throw new TypeError(`Missing parameters: ${missing.join(", ")}`);
 | |
|         }
 | |
|         return path;
 | |
|     };
 | |
| }
 | |
| function tokensToFunction(tokens, delimiter, encode) {
 | |
|     const encoders = tokens.map((token) => tokenToFunction(token, delimiter, encode));
 | |
|     return (data) => {
 | |
|         const result = [""];
 | |
|         for (const encoder of encoders) {
 | |
|             const [value, ...extras] = encoder(data);
 | |
|             result[0] += value;
 | |
|             result.push(...extras);
 | |
|         }
 | |
|         return result;
 | |
|     };
 | |
| }
 | |
| /**
 | |
|  * Convert a single token into a path building function.
 | |
|  */
 | |
| function tokenToFunction(token, delimiter, encode) {
 | |
|     if (token.type === "text")
 | |
|         return () => [token.value];
 | |
|     if (token.type === "group") {
 | |
|         const fn = tokensToFunction(token.tokens, delimiter, encode);
 | |
|         return (data) => {
 | |
|             const [value, ...missing] = fn(data);
 | |
|             if (!missing.length)
 | |
|                 return [value];
 | |
|             return [""];
 | |
|         };
 | |
|     }
 | |
|     const encodeValue = encode || NOOP_VALUE;
 | |
|     if (token.type === "wildcard" && encode !== false) {
 | |
|         return (data) => {
 | |
|             const value = data[token.name];
 | |
|             if (value == null)
 | |
|                 return ["", token.name];
 | |
|             if (!Array.isArray(value) || value.length === 0) {
 | |
|                 throw new TypeError(`Expected "${token.name}" to be a non-empty array`);
 | |
|             }
 | |
|             return [
 | |
|                 value
 | |
|                     .map((value, index) => {
 | |
|                     if (typeof value !== "string") {
 | |
|                         throw new TypeError(`Expected "${token.name}/${index}" to be a string`);
 | |
|                     }
 | |
|                     return encodeValue(value);
 | |
|                 })
 | |
|                     .join(delimiter),
 | |
|             ];
 | |
|         };
 | |
|     }
 | |
|     return (data) => {
 | |
|         const value = data[token.name];
 | |
|         if (value == null)
 | |
|             return ["", token.name];
 | |
|         if (typeof value !== "string") {
 | |
|             throw new TypeError(`Expected "${token.name}" to be a string`);
 | |
|         }
 | |
|         return [encodeValue(value)];
 | |
|     };
 | |
| }
 | |
| /**
 | |
|  * Transform a path into a match function.
 | |
|  */
 | |
| function match(path, options = {}) {
 | |
|     const { decode = decodeURIComponent, delimiter = DEFAULT_DELIMITER } = options;
 | |
|     const { regexp, keys } = pathToRegexp(path, options);
 | |
|     const decoders = keys.map((key) => {
 | |
|         if (decode === false)
 | |
|             return NOOP_VALUE;
 | |
|         if (key.type === "param")
 | |
|             return decode;
 | |
|         return (value) => value.split(delimiter).map(decode);
 | |
|     });
 | |
|     return function match(input) {
 | |
|         const m = regexp.exec(input);
 | |
|         if (!m)
 | |
|             return false;
 | |
|         const path = m[0];
 | |
|         const params = Object.create(null);
 | |
|         for (let i = 1; i < m.length; i++) {
 | |
|             if (m[i] === undefined)
 | |
|                 continue;
 | |
|             const key = keys[i - 1];
 | |
|             const decoder = decoders[i - 1];
 | |
|             params[key.name] = decoder(m[i]);
 | |
|         }
 | |
|         return { path, params };
 | |
|     };
 | |
| }
 | |
| function pathToRegexp(path, options = {}) {
 | |
|     const { delimiter = DEFAULT_DELIMITER, end = true, sensitive = false, trailing = true, } = options;
 | |
|     const keys = [];
 | |
|     const sources = [];
 | |
|     const flags = sensitive ? "" : "i";
 | |
|     const paths = Array.isArray(path) ? path : [path];
 | |
|     const items = paths.map((path) => path instanceof TokenData ? path : parse(path, options));
 | |
|     for (const { tokens } of items) {
 | |
|         for (const seq of flatten(tokens, 0, [])) {
 | |
|             const regexp = sequenceToRegExp(seq, delimiter, keys);
 | |
|             sources.push(regexp);
 | |
|         }
 | |
|     }
 | |
|     let pattern = `^(?:${sources.join("|")})`;
 | |
|     if (trailing)
 | |
|         pattern += `(?:${escape(delimiter)}$)?`;
 | |
|     pattern += end ? "$" : `(?=${escape(delimiter)}|$)`;
 | |
|     const regexp = new RegExp(pattern, flags);
 | |
|     return { regexp, keys };
 | |
| }
 | |
| /**
 | |
|  * Generate a flat list of sequence tokens from the given tokens.
 | |
|  */
 | |
| function* flatten(tokens, index, init) {
 | |
|     if (index === tokens.length) {
 | |
|         return yield init;
 | |
|     }
 | |
|     const token = tokens[index];
 | |
|     if (token.type === "group") {
 | |
|         const fork = init.slice();
 | |
|         for (const seq of flatten(token.tokens, 0, fork)) {
 | |
|             yield* flatten(tokens, index + 1, seq);
 | |
|         }
 | |
|     }
 | |
|     else {
 | |
|         init.push(token);
 | |
|     }
 | |
|     yield* flatten(tokens, index + 1, init);
 | |
| }
 | |
| /**
 | |
|  * Transform a flat sequence of tokens into a regular expression.
 | |
|  */
 | |
| function sequenceToRegExp(tokens, delimiter, keys) {
 | |
|     let result = "";
 | |
|     let backtrack = "";
 | |
|     let isSafeSegmentParam = true;
 | |
|     for (let i = 0; i < tokens.length; i++) {
 | |
|         const token = tokens[i];
 | |
|         if (token.type === "text") {
 | |
|             result += escape(token.value);
 | |
|             backtrack += token.value;
 | |
|             isSafeSegmentParam || (isSafeSegmentParam = token.value.includes(delimiter));
 | |
|             continue;
 | |
|         }
 | |
|         if (token.type === "param" || token.type === "wildcard") {
 | |
|             if (!isSafeSegmentParam && !backtrack) {
 | |
|                 throw new TypeError(`Missing text after "${token.name}": ${DEBUG_URL}`);
 | |
|             }
 | |
|             if (token.type === "param") {
 | |
|                 result += `(${negate(delimiter, isSafeSegmentParam ? "" : backtrack)}+)`;
 | |
|             }
 | |
|             else {
 | |
|                 result += `([\\s\\S]+)`;
 | |
|             }
 | |
|             keys.push(token);
 | |
|             backtrack = "";
 | |
|             isSafeSegmentParam = false;
 | |
|             continue;
 | |
|         }
 | |
|     }
 | |
|     return result;
 | |
| }
 | |
| function negate(delimiter, backtrack) {
 | |
|     if (backtrack.length < 2) {
 | |
|         if (delimiter.length < 2)
 | |
|             return `[^${escape(delimiter + backtrack)}]`;
 | |
|         return `(?:(?!${escape(delimiter)})[^${escape(backtrack)}])`;
 | |
|     }
 | |
|     if (delimiter.length < 2) {
 | |
|         return `(?:(?!${escape(backtrack)})[^${escape(delimiter)}])`;
 | |
|     }
 | |
|     return `(?:(?!${escape(backtrack)}|${escape(delimiter)})[\\s\\S])`;
 | |
| }
 | |
| /**
 | |
|  * Stringify token data into a path string.
 | |
|  */
 | |
| function stringify(data) {
 | |
|     return data.tokens
 | |
|         .map(function stringifyToken(token, index, tokens) {
 | |
|         if (token.type === "text")
 | |
|             return escapeText(token.value);
 | |
|         if (token.type === "group") {
 | |
|             return `{${token.tokens.map(stringifyToken).join("")}}`;
 | |
|         }
 | |
|         const isSafe = isNameSafe(token.name) && isNextNameSafe(tokens[index + 1]);
 | |
|         const key = isSafe ? token.name : JSON.stringify(token.name);
 | |
|         if (token.type === "param")
 | |
|             return `:${key}`;
 | |
|         if (token.type === "wildcard")
 | |
|             return `*${key}`;
 | |
|         throw new TypeError(`Unexpected token: ${token}`);
 | |
|     })
 | |
|         .join("");
 | |
| }
 | |
| function isNameSafe(name) {
 | |
|     const [first, ...rest] = name;
 | |
|     if (!ID_START.test(first))
 | |
|         return false;
 | |
|     return rest.every((char) => ID_CONTINUE.test(char));
 | |
| }
 | |
| function isNextNameSafe(token) {
 | |
|     if ((token === null || token === void 0 ? void 0 : token.type) !== "text")
 | |
|         return true;
 | |
|     return !ID_CONTINUE.test(token.value[0]);
 | |
| }
 | |
| //# sourceMappingURL=index.js.map
 |