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
 |