// Loaded from https://deno.land/x/path_to_regexp@v6.2.0/index.ts /** * Tokenizer results. */ interface LexToken { type: | "OPEN" | "CLOSE" | "PATTERN" | "NAME" | "CHAR" | "ESCAPED_CHAR" | "MODIFIER" | "END"; index: number; value: string; } /** * Tokenize input string. */ function lexer(str: string): LexToken[] { const tokens: LexToken[] = []; let i = 0; while (i < str.length) { const char = str[i]; if (char === "*" || char === "+" || char === "?") { tokens.push({ type: "MODIFIER", index: i, value: str[i++] }); continue; } if (char === "\\") { tokens.push({ type: "ESCAPED_CHAR", index: i++, value: str[i++] }); continue; } if (char === "{") { tokens.push({ type: "OPEN", index: i, value: str[i++] }); continue; } if (char === "}") { tokens.push({ type: "CLOSE", index: i, value: str[i++] }); continue; } if (char === ":") { let name = ""; let j = i + 1; while (j < str.length) { const code = str.charCodeAt(j); if ( // `0-9` (code >= 48 && code <= 57) || // `A-Z` (code >= 65 && code <= 90) || // `a-z` (code >= 97 && code <= 122) || // `_` code === 95 ) { name += str[j++]; continue; } break; } if (!name) throw new TypeError(`Missing parameter name at ${i}`); tokens.push({ type: "NAME", index: i, value: name }); i = j; continue; } if (char === "(") { let count = 1; let pattern = ""; let j = i + 1; if (str[j] === "?") { throw new TypeError(`Pattern cannot start with "?" at ${j}`); } while (j < str.length) { if (str[j] === "\\") { pattern += str[j++] + str[j++]; continue; } if (str[j] === ")") { count--; if (count === 0) { j++; break; } } else if (str[j] === "(") { count++; if (str[j + 1] !== "?") { throw new TypeError(`Capturing groups are not allowed at ${j}`); } } pattern += str[j++]; } if (count) throw new TypeError(`Unbalanced pattern at ${i}`); if (!pattern) throw new TypeError(`Missing pattern at ${i}`); tokens.push({ type: "PATTERN", index: i, value: pattern }); i = j; continue; } tokens.push({ type: "CHAR", index: i, value: str[i++] }); } tokens.push({ type: "END", index: i, value: "" }); return tokens; } export interface ParseOptions { /** * Set the default delimiter for repeat parameters. (default: `'/'`) */ delimiter?: string; /** * List of characters to automatically consider prefixes when parsing. */ prefixes?: string; } /** * Parse a string for the raw tokens. */ export function parse(str: string, options: ParseOptions = {}): Token[] { const tokens = lexer(str); const { prefixes = "./" } = options; const defaultPattern = `[^${escapeString(options.delimiter || "/#?")}]+?`; const result: Token[] = []; let key = 0; let i = 0; let path = ""; const tryConsume = (type: LexToken["type"]): string | undefined => { if (i < tokens.length && tokens[i].type === type) return tokens[i++].value; }; const mustConsume = (type: LexToken["type"]): string => { const value = tryConsume(type); if (value !== undefined) return value; const { type: nextType, index } = tokens[i]; throw new TypeError(`Unexpected ${nextType} at ${index}, expected ${type}`); }; const consumeText = (): string => { let result = ""; let value: string | undefined; // tslint:disable-next-line while ((value = tryConsume("CHAR") || tryConsume("ESCAPED_CHAR"))) { result += value; } return result; }; while (i < tokens.length) { const char = tryConsume("CHAR"); const name = tryConsume("NAME"); const pattern = tryConsume("PATTERN"); if (name || pattern) { let prefix = char || ""; if (prefixes.indexOf(prefix) === -1) { path += prefix; prefix = ""; } if (path) { result.push(path); path = ""; } result.push({ name: name || key++, prefix, suffix: "", pattern: pattern || defaultPattern, modifier: tryConsume("MODIFIER") || "" }); continue; } const value = char || tryConsume("ESCAPED_CHAR"); if (value) { path += value; continue; } if (path) { result.push(path); path = ""; } const open = tryConsume("OPEN"); if (open) { const prefix = consumeText(); const name = tryConsume("NAME") || ""; const pattern = tryConsume("PATTERN") || ""; const suffix = consumeText(); mustConsume("CLOSE"); result.push({ name: name || (pattern ? key++ : ""), pattern: name && !pattern ? defaultPattern : pattern, prefix, suffix, modifier: tryConsume("MODIFIER") || "" }); continue; } mustConsume("END"); } return result; } export interface TokensToFunctionOptions { /** * When `true` the regexp will be case sensitive. (default: `false`) */ sensitive?: boolean; /** * Function for encoding input strings for output. */ encode?: (value: string, token: Key) => string; /** * When `false` the function can produce an invalid (unmatched) path. (default: `true`) */ validate?: boolean; } /** * Compile a string to a template function for the path. */ export function compile
( str: string, options?: ParseOptions & TokensToFunctionOptions ) { return tokensToFunction
(parse(str, options), options); } export type PathFunction
= (data?: P) => string; /** * Expose a method for transforming tokens into the path function. */ export function tokensToFunction
( tokens: Token[], options: TokensToFunctionOptions = {} ): PathFunction
{
const reFlags = flags(options);
const { encode = (x: string) => x, validate = true } = options;
// Compile all the tokens into regexps.
const matches = tokens.map(token => {
if (typeof token === "object") {
return new RegExp(`^(?:${token.pattern})$`, reFlags);
}
});
return (data: Record {
path: string;
index: number;
params: P;
}
/**
* A match is either `false` (no match) or a match result.
*/
export type Match = false | MatchResult ;
/**
* The match function takes a string and returns whether it matched the path.
*/
export type MatchFunction = (
path: string
) => Match ;
/**
* Create path match function from `path-to-regexp` spec.
*/
export function match (
str: Path,
options?: ParseOptions & TokensToRegexpOptions & RegexpToFunctionOptions
) {
const keys: Key[] = [];
const re = pathToRegexp(str, keys, options);
return regexpToFunction (re, keys, options);
}
/**
* Create a path match function from `path-to-regexp` output.
*/
export function regexpToFunction (
re: RegExp,
keys: Key[],
options: RegexpToFunctionOptions = {}
): MatchFunction {
const { decode = (x: string) => x } = options;
return function(pathname: string) {
const m = re.exec(pathname);
if (!m) return false;
const { 0: path, index } = m;
const params = Object.create(null);
for (let i = 1; i < m.length; i++) {
// tslint:disable-next-line
if (m[i] === undefined) continue;
const key = keys[i - 1];
if (key.modifier === "*" || key.modifier === "+") {
params[key.name] = m[i].split(key.prefix + key.suffix).map(value => {
return decode(value, key);
});
} else {
params[key.name] = decode(m[i], key);
}
}
return { path, index, params };
};
}
/**
* Escape a regular expression string.
*/
function escapeString(str: string) {
return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1");
}
/**
* Get the flags for a regexp from the options.
*/
function flags(options?: { sensitive?: boolean }) {
return options && options.sensitive ? "" : "i";
}
/**
* Metadata about a key.
*/
export interface Key {
name: string | number;
prefix: string;
suffix: string;
pattern: string;
modifier: string;
}
/**
* A token is a string (nothing special) or key metadata (capture group).
*/
export type Token = string | Key;
/**
* Pull out keys from a regexp.
*/
function regexpToRegexp(path: RegExp, keys?: Key[]): RegExp {
if (!keys) return path;
const groupsRegex = /\((?:\?<(.*?)>)?(?!\?)/g;
let index = 0;
let execResult = groupsRegex.exec(path.source);
while (execResult) {
keys.push({
// Use parenthesized substring match if available, index otherwise
name: execResult[1] || index++,
prefix: "",
suffix: "",
modifier: "",
pattern: ""
});
execResult = groupsRegex.exec(path.source);
}
return path;
}
/**
* Transform an array into a regexp.
*/
function arrayToRegexp(
paths: Array