Spaces:
No application file
No application file
| ; | |
| 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 |