path-to-regexp
Advanced tools
Comparing version 7.1.0 to 8.0.0
@@ -11,6 +11,2 @@ /** | ||
/** | ||
* The default delimiter for segments. (default: `'/'`) | ||
*/ | ||
delimiter?: string; | ||
/** | ||
* A function for encoding input strings. | ||
@@ -20,63 +16,72 @@ */ | ||
} | ||
export interface PathToRegexpOptions extends ParseOptions { | ||
export interface MatchOptions { | ||
/** | ||
* Regexp will be case sensitive. (default: `false`) | ||
* Function for decoding strings for params, or `false` to disable entirely. (default: `decodeURIComponent`) | ||
*/ | ||
sensitive?: boolean; | ||
decode?: Decode | false; | ||
/** | ||
* Allow the delimiter to be arbitrarily repeated. (default: `true`) | ||
* Matches the path completely without trailing characters. (default: `true`) | ||
*/ | ||
loose?: boolean; | ||
/** | ||
* Verify patterns are valid and safe to use. (default: `false`) | ||
*/ | ||
strict?: boolean; | ||
/** | ||
* Match from the beginning of the string. (default: `true`) | ||
*/ | ||
start?: boolean; | ||
/** | ||
* Match to the end of the string. (default: `true`) | ||
*/ | ||
end?: boolean; | ||
/** | ||
* Allow optional trailing delimiter to match. (default: `true`) | ||
* Allows optional trailing delimiter to match. (default: `true`) | ||
*/ | ||
trailing?: boolean; | ||
} | ||
export interface MatchOptions extends PathToRegexpOptions { | ||
/** | ||
* Function for decoding strings for params, or `false` to disable entirely. (default: `decodeURIComponent`) | ||
* Match will be case sensitive. (default: `false`) | ||
*/ | ||
decode?: Decode | false; | ||
} | ||
export interface CompileOptions extends ParseOptions { | ||
/** | ||
* Regexp will be case sensitive. (default: `false`) | ||
*/ | ||
sensitive?: boolean; | ||
/** | ||
* Allow the delimiter to be arbitrarily repeated. (default: `true`) | ||
* The default delimiter for segments. (default: `'/'`) | ||
*/ | ||
loose?: boolean; | ||
delimiter?: string; | ||
} | ||
export interface CompileOptions { | ||
/** | ||
* Verify patterns are valid and safe to use. (default: `false`) | ||
* Function for encoding input strings for output into the path, or `false` to disable entirely. (default: `encodeURIComponent`) | ||
*/ | ||
strict?: boolean; | ||
encode?: Encode | false; | ||
/** | ||
* Verifies the function is producing a valid path. (default: `true`) | ||
* The default delimiter for segments. (default: `'/'`) | ||
*/ | ||
validate?: boolean; | ||
/** | ||
* Function for encoding input strings for output into the path, or `false` to disable entirely. (default: `encodeURIComponent`) | ||
*/ | ||
encode?: Encode | false; | ||
delimiter?: string; | ||
} | ||
/** | ||
* Tokenized path instance. Can we passed around instead of string. | ||
* Plain text. | ||
*/ | ||
export interface Text { | ||
type: "text"; | ||
value: string; | ||
} | ||
/** | ||
* A parameter designed to match arbitrary text within a segment. | ||
*/ | ||
export interface Parameter { | ||
type: "param"; | ||
name: string; | ||
} | ||
/** | ||
* A wildcard parameter designed to match multiple segments. | ||
*/ | ||
export interface Wildcard { | ||
type: "wildcard"; | ||
name: string; | ||
} | ||
/** | ||
* A set of possible tokens to expand when matching. | ||
*/ | ||
export interface Group { | ||
type: "group"; | ||
tokens: Token[]; | ||
} | ||
/** | ||
* A sequence of path match characters. | ||
*/ | ||
export type Token = Text | Parameter | Wildcard | Group; | ||
/** | ||
* Tokenized path instance. | ||
*/ | ||
export declare class TokenData { | ||
readonly tokens: Token[]; | ||
readonly delimiter: string; | ||
constructor(tokens: Token[], delimiter: string); | ||
constructor(tokens: Token[]); | ||
} | ||
@@ -90,3 +95,3 @@ /** | ||
*/ | ||
export declare function compile<P extends ParamData = ParamData>(path: Path, options?: CompileOptions): PathFunction<P>; | ||
export declare function compile<P extends ParamData = ParamData>(path: Path, options?: CompileOptions & ParseOptions): PathFunction<P>; | ||
export type ParamData = Partial<Record<string, string | string[]>>; | ||
@@ -99,3 +104,2 @@ export type PathFunction<P extends ParamData> = (data?: P) => string; | ||
path: string; | ||
index: number; | ||
params: P; | ||
@@ -111,34 +115,3 @@ } | ||
export type MatchFunction<P extends ParamData> = (path: string) => Match<P>; | ||
/** | ||
* Create path match function from `path-to-regexp` spec. | ||
*/ | ||
export declare function match<P extends ParamData>(path: Path, options?: MatchOptions): MatchFunction<P>; | ||
/** | ||
* A key is a capture group in the regex. | ||
*/ | ||
export interface Key { | ||
name: string; | ||
prefix?: string; | ||
suffix?: string; | ||
pattern?: string; | ||
modifier?: string; | ||
separator?: string; | ||
} | ||
/** | ||
* A token is a string (nothing special) or key metadata (capture group). | ||
*/ | ||
export type Token = string | Key; | ||
/** | ||
* Repeated and simple input types. | ||
*/ | ||
export type Path = string | TokenData; | ||
/** | ||
* Normalize the given path string, returning a regular expression. | ||
* | ||
* An empty array can be passed in for the keys, which will hold the | ||
* placeholder key descriptions. For example, using `/user/:id`, `keys` will | ||
* contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`. | ||
*/ | ||
export declare function pathToRegexp(path: Path, options?: PathToRegexpOptions): RegExp & { | ||
keys: Key[]; | ||
}; | ||
export declare function match<P extends ParamData>(path: Path | Path[], options?: MatchOptions & ParseOptions): MatchFunction<P>; |
"use strict"; | ||
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { | ||
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); | ||
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); | ||
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); | ||
}; | ||
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { | ||
if (kind === "m") throw new TypeError("Private method is not writable"); | ||
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); | ||
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); | ||
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; | ||
}; | ||
var _Iter_peek; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -7,85 +19,92 @@ exports.TokenData = void 0; | ||
exports.match = match; | ||
exports.pathToRegexp = pathToRegexp; | ||
const DEFAULT_DELIMITER = "/"; | ||
const NOOP_VALUE = (value) => value; | ||
const ID_CHAR = /^\p{XID_Continue}$/u; | ||
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 a regular expression string. | ||
*/ | ||
function escape(str) { | ||
return str.replace(/[.+*?^${}()[\]|/\\]/g, "\\$&"); | ||
} | ||
/** | ||
* Get the flags for a regexp from the options. | ||
*/ | ||
function toFlags(options) { | ||
return options.sensitive ? "s" : "is"; | ||
} | ||
/** | ||
* Tokenize input string. | ||
*/ | ||
function lexer(str) { | ||
function* lexer(str) { | ||
const chars = [...str]; | ||
const tokens = []; | ||
let i = 0; | ||
while (i < chars.length) { | ||
const value = chars[i]; | ||
const type = SIMPLE_TOKENS[value]; | ||
if (type) { | ||
tokens.push({ type, index: i++, value }); | ||
continue; | ||
} | ||
if (value === "\\") { | ||
tokens.push({ type: "ESCAPED", index: i++, value: chars[i++] }); | ||
continue; | ||
} | ||
if (value === ":") { | ||
let name = ""; | ||
while (ID_CHAR.test(chars[++i])) { | ||
name += chars[i]; | ||
function name() { | ||
let value = ""; | ||
if (ID_START.test(chars[++i])) { | ||
value += chars[i]; | ||
while (ID_CONTINUE.test(chars[++i])) { | ||
value += chars[i]; | ||
} | ||
if (!name) { | ||
throw new TypeError(`Missing parameter name at ${i}`); | ||
} | ||
tokens.push({ type: "NAME", index: i, value: name }); | ||
continue; | ||
} | ||
if (value === "(") { | ||
const pos = i++; | ||
let count = 1; | ||
let pattern = ""; | ||
if (chars[i] === "?") { | ||
throw new TypeError(`Pattern cannot start with "?" at ${i}`); | ||
} | ||
else if (chars[i] === '"') { | ||
let pos = i; | ||
while (i < chars.length) { | ||
if (chars[++i] === '"') { | ||
i++; | ||
pos = 0; | ||
break; | ||
} | ||
if (chars[i] === "\\") { | ||
pattern += chars[i++] + chars[i++]; | ||
continue; | ||
value += chars[++i]; | ||
} | ||
if (chars[i] === ")") { | ||
count--; | ||
if (count === 0) { | ||
i++; | ||
break; | ||
} | ||
else { | ||
value += chars[i]; | ||
} | ||
else if (chars[i] === "(") { | ||
count++; | ||
if (chars[i + 1] !== "?") { | ||
throw new TypeError(`Capturing groups are not allowed at ${i}`); | ||
} | ||
} | ||
pattern += chars[i++]; | ||
} | ||
if (count) | ||
throw new TypeError(`Unbalanced pattern at ${pos}`); | ||
if (!pattern) | ||
throw new TypeError(`Missing pattern at ${pos}`); | ||
tokens.push({ type: "PATTERN", index: i, value: pattern }); | ||
continue; | ||
if (pos) { | ||
throw new TypeError(`Unterminated quote at ${pos}: ${DEBUG_URL}`); | ||
} | ||
} | ||
tokens.push({ type: "CHAR", index: i, value: chars[i++] }); | ||
if (!value) { | ||
throw new TypeError(`Missing parameter name at ${i}: ${DEBUG_URL}`); | ||
} | ||
return value; | ||
} | ||
tokens.push({ type: "END", index: i, value: "" }); | ||
return new Iter(tokens); | ||
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: "" }; | ||
} | ||
@@ -95,6 +114,10 @@ class Iter { | ||
this.tokens = tokens; | ||
this.index = 0; | ||
_Iter_peek.set(this, void 0); | ||
} | ||
peek() { | ||
return this.tokens[this.index]; | ||
if (!__classPrivateFieldGet(this, _Iter_peek, "f")) { | ||
const next = this.tokens.next(); | ||
__classPrivateFieldSet(this, _Iter_peek, next.value, "f"); | ||
} | ||
return __classPrivateFieldGet(this, _Iter_peek, "f"); | ||
} | ||
@@ -105,3 +128,3 @@ tryConsume(type) { | ||
return; | ||
this.index++; | ||
__classPrivateFieldSet(this, _Iter_peek, undefined, "f"); // Reset after consumed. | ||
return token.value; | ||
@@ -124,13 +147,10 @@ } | ||
} | ||
modifier() { | ||
return this.tryConsume("?") || this.tryConsume("*") || this.tryConsume("+"); | ||
} | ||
} | ||
_Iter_peek = new WeakMap(); | ||
/** | ||
* Tokenized path instance. Can we passed around instead of string. | ||
* Tokenized path instance. | ||
*/ | ||
class TokenData { | ||
constructor(tokens, delimiter) { | ||
constructor(tokens) { | ||
this.tokens = tokens; | ||
this.delimiter = delimiter; | ||
} | ||
@@ -143,56 +163,54 @@ } | ||
function parse(str, options = {}) { | ||
const { encodePath = NOOP_VALUE, delimiter = encodePath(DEFAULT_DELIMITER) } = options; | ||
const tokens = []; | ||
const it = lexer(str); | ||
let key = 0; | ||
do { | ||
const path = it.text(); | ||
if (path) | ||
tokens.push(encodePath(path)); | ||
const name = it.tryConsume("NAME"); | ||
const pattern = it.tryConsume("PATTERN"); | ||
if (name || pattern) { | ||
tokens.push({ | ||
name: name || String(key++), | ||
pattern, | ||
}); | ||
const next = it.peek(); | ||
if (next.type === "*") { | ||
throw new TypeError(`Unexpected * at ${next.index}, you probably want \`/*\` or \`{/:foo}*\`: ${DEBUG_URL}`); | ||
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; | ||
} | ||
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 asterisk = it.tryConsume("*"); | ||
if (asterisk) { | ||
tokens.push({ | ||
name: String(key++), | ||
pattern: `(?:(?!${escape(delimiter)}).)*`, | ||
modifier: "*", | ||
separator: delimiter, | ||
}); | ||
continue; | ||
} | ||
const tokens = consume("END"); | ||
return new TokenData(tokens); | ||
} | ||
/** | ||
* Transform tokens into a path building function. | ||
*/ | ||
function $compile(data, options) { | ||
const { encode = encodeURIComponent, delimiter = DEFAULT_DELIMITER } = 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(", ")}`); | ||
} | ||
const open = it.tryConsume("{"); | ||
if (open) { | ||
const prefix = it.text(); | ||
const name = it.tryConsume("NAME"); | ||
const pattern = it.tryConsume("PATTERN"); | ||
const suffix = it.text(); | ||
const separator = it.tryConsume(";") && it.text(); | ||
it.consume("}"); | ||
const modifier = it.modifier(); | ||
tokens.push({ | ||
name: name || (pattern ? String(key++) : ""), | ||
prefix: encodePath(prefix), | ||
suffix: encodePath(suffix), | ||
pattern, | ||
modifier, | ||
separator, | ||
}); | ||
continue; | ||
} | ||
it.consume("END"); | ||
break; | ||
} while (true); | ||
return new TokenData(tokens, delimiter); | ||
return path; | ||
}; | ||
} | ||
@@ -203,56 +221,50 @@ /** | ||
function compile(path, options = {}) { | ||
const data = path instanceof TokenData ? path : parse(path, options); | ||
return compileTokens(data, options); | ||
return $compile(path instanceof TokenData ? path : parse(path, options), options); | ||
} | ||
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, encode) { | ||
if (typeof token === "string") { | ||
return () => token; | ||
} | ||
const encodeValue = encode || NOOP_VALUE; | ||
const repeated = token.modifier === "+" || token.modifier === "*"; | ||
const optional = token.modifier === "?" || token.modifier === "*"; | ||
const { prefix = "", suffix = "", separator = suffix + prefix } = token; | ||
if (encode && repeated) { | ||
const stringify = (value, index) => { | ||
if (typeof value !== "string") { | ||
throw new TypeError(`Expected "${token.name}/${index}" to be a string`); | ||
} | ||
return encodeValue(value); | ||
}; | ||
const compile = (value) => { | ||
if (!Array.isArray(value)) { | ||
throw new TypeError(`Expected "${token.name}" to be an array`); | ||
} | ||
if (value.length === 0) | ||
return ""; | ||
return prefix + value.map(stringify).join(separator) + suffix; | ||
}; | ||
if (optional) { | ||
return (data) => { | ||
const value = data[token.name]; | ||
if (value == null) | ||
return ""; | ||
return value.length ? compile(value) : ""; | ||
}; | ||
} | ||
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 = data[token.name]; | ||
return compile(value); | ||
const [value, ...missing] = fn(data); | ||
if (!missing.length) | ||
return [value]; | ||
return [""]; | ||
}; | ||
} | ||
const stringify = (value) => { | ||
if (typeof value !== "string") { | ||
throw new TypeError(`Expected "${token.name}" to be a string`); | ||
} | ||
return prefix + encodeValue(value) + suffix; | ||
}; | ||
if (optional) { | ||
const encodeValue = encode || NOOP_VALUE; | ||
if (token.type === "wildcard" && encode !== false) { | ||
return (data) => { | ||
const value = data[token.name]; | ||
if (value == null) | ||
return ""; | ||
return stringify(value); | ||
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), | ||
]; | ||
}; | ||
@@ -262,56 +274,41 @@ } | ||
const value = data[token.name]; | ||
return stringify(value); | ||
if (value == null) | ||
return ["", token.name]; | ||
if (typeof value !== "string") { | ||
throw new TypeError(`Expected "${token.name}" to be a string`); | ||
} | ||
return [encodeValue(value)]; | ||
}; | ||
} | ||
/** | ||
* Transform tokens into a path building function. | ||
* Create path match function from `path-to-regexp` spec. | ||
*/ | ||
function compileTokens(data, options) { | ||
const { encode = encodeURIComponent, loose = true, validate = true, strict = false, } = options; | ||
function $match(data, options = {}) { | ||
const { decode = decodeURIComponent, delimiter = DEFAULT_DELIMITER, end = true, trailing = true, } = options; | ||
const flags = toFlags(options); | ||
const stringify = toStringify(loose, data.delimiter); | ||
const sources = toRegExpSource(data, stringify, [], flags, strict); | ||
// Compile all the tokens into regexps. | ||
const encoders = data.tokens.map((token, index) => { | ||
const fn = tokenToFunction(token, encode); | ||
if (!validate || typeof token === "string") | ||
return fn; | ||
const validRe = new RegExp(`^${sources[index]}$`, flags); | ||
return (data) => { | ||
const value = fn(data); | ||
if (!validRe.test(value)) { | ||
throw new TypeError(`Invalid value for "${token.name}": ${JSON.stringify(value)}`); | ||
} | ||
return value; | ||
}; | ||
}); | ||
return function path(data = {}) { | ||
let path = ""; | ||
for (const encoder of encoders) | ||
path += encoder(data); | ||
return path; | ||
}; | ||
} | ||
/** | ||
* Create path match function from `path-to-regexp` spec. | ||
*/ | ||
function match(path, options = {}) { | ||
const { decode = decodeURIComponent, loose = true } = options; | ||
const data = path instanceof TokenData ? path : parse(path, options); | ||
const stringify = toStringify(loose, data.delimiter); | ||
const sources = []; | ||
const keys = []; | ||
const re = tokensToRegexp(data, keys, options); | ||
for (const { tokens } of data) { | ||
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 re = new RegExp(pattern, flags); | ||
const decoders = keys.map((key) => { | ||
if (decode && (key.modifier === "+" || key.modifier === "*")) { | ||
const { prefix = "", suffix = "", separator = suffix + prefix } = key; | ||
const re = new RegExp(stringify(separator), "g"); | ||
return (value) => value.split(re).map(decode); | ||
} | ||
return decode || NOOP_VALUE; | ||
if (decode === false) | ||
return NOOP_VALUE; | ||
if (key.type === "param") | ||
return decode; | ||
return (value) => value.split(delimiter).map(decode); | ||
}); | ||
return function match(input) { | ||
return Object.assign(function match(input) { | ||
const m = re.exec(input); | ||
if (!m) | ||
return false; | ||
const { 0: path, index } = m; | ||
const { 0: path } = m; | ||
const params = Object.create(null); | ||
@@ -325,115 +322,69 @@ for (let i = 1; i < m.length; i++) { | ||
} | ||
return { path, index, params }; | ||
}; | ||
return { path, params }; | ||
}, { re }); | ||
} | ||
/** | ||
* Escape a regular expression string. | ||
*/ | ||
function escape(str) { | ||
return str.replace(/([.+*?^${}()[\]|/\\])/g, "\\$1"); | ||
function match(path, options = {}) { | ||
const paths = Array.isArray(path) ? path : [path]; | ||
const items = paths.map((path) => path instanceof TokenData ? path : parse(path, options)); | ||
return $match(items, options); | ||
} | ||
/** | ||
* Escape and repeat loose characters for regular expressions. | ||
* Generate a flat list of sequence tokens from the given tokens. | ||
*/ | ||
function looseReplacer(value, loose) { | ||
const escaped = escape(value); | ||
return loose ? `(?:${escaped})+(?!${escaped})` : escaped; | ||
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); | ||
} | ||
/** | ||
* Encode all non-delimiter characters using the encode function. | ||
* Transform a flat sequence of tokens into a regular expression. | ||
*/ | ||
function toStringify(loose, delimiter) { | ||
if (!loose) | ||
return escape; | ||
const re = new RegExp(`(?:(?!${escape(delimiter)}).)+|(.)`, "g"); | ||
return (value) => value.replace(re, looseReplacer); | ||
} | ||
/** | ||
* Get the flags for a regexp from the options. | ||
*/ | ||
function toFlags(options) { | ||
return options.sensitive ? "" : "i"; | ||
} | ||
/** | ||
* Expose a function for taking tokens and returning a RegExp. | ||
*/ | ||
function tokensToRegexp(data, keys, options) { | ||
const { trailing = true, loose = true, start = true, end = true, strict = false, } = options; | ||
const flags = toFlags(options); | ||
const stringify = toStringify(loose, data.delimiter); | ||
const sources = toRegExpSource(data, stringify, keys, flags, strict); | ||
let pattern = start ? "^" : ""; | ||
pattern += sources.join(""); | ||
if (trailing) | ||
pattern += `(?:${stringify(data.delimiter)})?`; | ||
pattern += end ? "$" : `(?=${escape(data.delimiter)}|$)`; | ||
return new RegExp(pattern, flags); | ||
} | ||
/** | ||
* Convert a token into a regexp string (re-used for path validation). | ||
*/ | ||
function toRegExpSource(data, stringify, keys, flags, strict) { | ||
const defaultPattern = `(?:(?!${escape(data.delimiter)}).)+?`; | ||
function sequenceToRegExp(tokens, delimiter, keys) { | ||
let result = ""; | ||
let backtrack = ""; | ||
let safe = true; | ||
return data.tokens.map((token, index) => { | ||
if (typeof token === "string") { | ||
backtrack = token; | ||
return stringify(token); | ||
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; | ||
} | ||
const { prefix = "", suffix = "", separator = suffix + prefix, modifier = "", } = token; | ||
const pre = stringify(prefix); | ||
const post = stringify(suffix); | ||
if (token.name) { | ||
const pattern = token.pattern ? `(?:${token.pattern})` : defaultPattern; | ||
const re = checkPattern(pattern, token.name, flags); | ||
safe || (safe = safePattern(re, prefix || backtrack)); | ||
if (!safe) { | ||
throw new TypeError(`Ambiguous pattern for "${token.name}": ${DEBUG_URL}`); | ||
if (token.type === "param" || token.type === "wildcard") { | ||
if (!isSafeSegmentParam && !backtrack) { | ||
throw new TypeError(`Missing text after "${token.name}": ${DEBUG_URL}`); | ||
} | ||
safe = !strict || safePattern(re, suffix); | ||
if (token.type === "param") { | ||
result += `(${negate(delimiter, isSafeSegmentParam ? "" : backtrack)}+)`; | ||
} | ||
else { | ||
result += `(.+)`; | ||
} | ||
keys.push(token); | ||
backtrack = ""; | ||
keys.push(token); | ||
if (modifier === "+" || modifier === "*") { | ||
const mod = modifier === "*" ? "?" : ""; | ||
const sep = stringify(separator); | ||
if (!sep) { | ||
throw new TypeError(`Missing separator for "${token.name}": ${DEBUG_URL}`); | ||
} | ||
safe || (safe = !strict || safePattern(re, separator)); | ||
if (!safe) { | ||
throw new TypeError(`Ambiguous pattern for "${token.name}" separator: ${DEBUG_URL}`); | ||
} | ||
safe = !strict; | ||
return `(?:${pre}(${pattern}(?:${sep}${pattern})*)${post})${mod}`; | ||
} | ||
return `(?:${pre}(${pattern})${post})${modifier}`; | ||
isSafeSegmentParam = false; | ||
continue; | ||
} | ||
return `(?:${pre}${post})${modifier}`; | ||
}); | ||
} | ||
function checkPattern(pattern, name, flags) { | ||
try { | ||
return new RegExp(`^${pattern}$`, flags); | ||
} | ||
catch (err) { | ||
throw new TypeError(`Invalid pattern for "${name}": ${err.message}`); | ||
} | ||
return result; | ||
} | ||
function safePattern(re, value) { | ||
return value ? !re.test(value) : false; | ||
function negate(delimiter, backtrack) { | ||
const values = [delimiter, backtrack].filter(Boolean); | ||
const isSimple = values.every((value) => value.length === 1); | ||
if (isSimple) | ||
return `[^${escape(values.join(""))}]`; | ||
return `(?:(?!${values.map(escape).join("|")}).)`; | ||
} | ||
/** | ||
* Normalize the given path string, returning a regular expression. | ||
* | ||
* An empty array can be passed in for the keys, which will hold the | ||
* placeholder key descriptions. For example, using `/user/:id`, `keys` will | ||
* contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`. | ||
*/ | ||
function pathToRegexp(path, options = {}) { | ||
const data = path instanceof TokenData ? path : parse(path, options); | ||
const keys = []; | ||
const regexp = tokensToRegexp(data, keys, options); | ||
return Object.assign(regexp, { keys }); | ||
} | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "path-to-regexp", | ||
"version": "7.1.0", | ||
"version": "8.0.0", | ||
"description": "Express style path to RegExp utility", | ||
@@ -23,2 +23,3 @@ "keywords": [ | ||
"scripts": { | ||
"bench": "vitest bench", | ||
"build": "ts-scripts build", | ||
@@ -25,0 +26,0 @@ "format": "ts-scripts format", |
269
Readme.md
@@ -20,204 +20,68 @@ # Path-to-RegExp | ||
```js | ||
const { pathToRegexp, match, parse, compile } = require("path-to-regexp"); | ||
const { match, compile, parse } = require("path-to-regexp"); | ||
// pathToRegexp(path, options?) | ||
// match(path, options?) | ||
// compile(path, options?) | ||
// parse(path, options?) | ||
// compile(path, options?) | ||
``` | ||
### Path to regexp | ||
The `pathToRegexp` function returns a regular expression with `keys` as a property. It accepts the following arguments: | ||
- **path** A string. | ||
- **options** _(optional)_ | ||
- **sensitive** Regexp will be case sensitive. (default: `false`) | ||
- **trailing** Allows optional trailing delimiter to match. (default: `true`) | ||
- **strict** Verify patterns are valid and safe to use. (default: `false`, recommended: `true`) | ||
- **end** Match to the end of the string. (default: `true`) | ||
- **start** Match from the beginning of the string. (default: `true`) | ||
- **loose** Allow the delimiter to be arbitrarily repeated, e.g. `/` or `///`. (default: `true`) | ||
- **delimiter** The default delimiter for segments, e.g. `[^/]` for `:named` parameters. (default: `'/'`) | ||
- **encodePath** A function for encoding input strings. (default: `x => x`, recommended: [`encodeurl`](https://github.com/pillarjs/encodeurl) for unicode encoding) | ||
```js | ||
const regexp = pathToRegexp("/foo/:bar"); | ||
// regexp = /^\/+foo(?:\/+([^\/]+?))(?:\/+)?$/i | ||
// keys = [{ name: 'bar', prefix: '', suffix: '', pattern: '', modifier: '' }] | ||
``` | ||
**Please note:** The `RegExp` returned by `path-to-regexp` is intended for ordered data (e.g. pathnames, hostnames). It can not handle arbitrarily ordered data (e.g. query strings, URL fragments, JSON, etc). | ||
### Parameters | ||
The path argument is used to define parameters and populate keys. | ||
Parameters match arbitrary strings in a path by matching up to the end of the segment, or up to any proceeding tokens. They are defined by prefixing a colon to the parameter name (`:foo`). Parameter names can use any valid JavaScript identifier, or be double quoted to use other characters (`:"param-name"`). | ||
#### Named parameters | ||
Named parameters are defined by prefixing a colon to the parameter name (`:foo`). Parameter names can use any valid unicode identifier characters (similar to JavaScript). | ||
```js | ||
const regexp = pathToRegexp("/:foo/:bar"); | ||
// keys = [{ name: 'foo', ... }, { name: 'bar', ... }] | ||
const fn = match("/:foo/:bar"); | ||
regexp.exec("/test/route"); | ||
//=> [ '/test/route', 'test', 'route', index: 0 ] | ||
fn("/test/route"); | ||
//=> { path: '/test/route', params: { foo: 'test', bar: 'route' } } | ||
``` | ||
##### Custom matching parameters | ||
### Wildcard | ||
Parameters can have a custom regexp, which overrides the default match (`[^/]+`). For example, you can match digits or names in a path: | ||
Wildcard parameters match one or more characters across multiple segments. They are defined the same way as regular parameters, but are prefixed with an asterisk (`*foo`). | ||
```js | ||
const regexpNumbers = pathToRegexp("/icon-:foo(\\d+).png"); | ||
// keys = [{ name: 'foo', ... }] | ||
const fn = match("/*splat"); | ||
regexpNumbers.exec("/icon-123.png"); | ||
//=> ['/icon-123.png', '123'] | ||
regexpNumbers.exec("/icon-abc.png"); | ||
//=> null | ||
const regexpWord = pathToRegexp("/(user|u)"); | ||
// keys = [{ name: 0, ... }] | ||
regexpWord.exec("/u"); | ||
//=> ['/u', 'u'] | ||
regexpWord.exec("/users"); | ||
//=> null | ||
fn("/bar/baz"); | ||
//=> { path: '/bar/baz', params: { splat: [ 'bar', 'baz' ] } } | ||
``` | ||
**Tip:** Backslashes need to be escaped with another backslash in JavaScript strings. | ||
### Optional | ||
#### Unnamed parameters | ||
Braces can be used to define parts of the path that are optional. | ||
It is possible to define a parameter without a name. The name will be numerically indexed: | ||
```js | ||
const regexp = pathToRegexp("/:foo/(.*)"); | ||
// keys = [{ name: 'foo', ... }, { name: '0', ... }] | ||
const fn = match("/users{/:id}/delete"); | ||
regexp.exec("/test/route"); | ||
//=> [ '/test/route', 'test', 'route', index: 0 ] | ||
``` | ||
fn("/users/delete"); | ||
//=> { path: '/users/delete', params: {} } | ||
##### Custom prefix and suffix | ||
Parameters can be wrapped in `{}` to create custom prefixes or suffixes for your segment: | ||
```js | ||
const regexp = pathToRegexp("{/:attr1}?{-:attr2}?{-:attr3}?"); | ||
regexp.exec("/test"); | ||
// => ['/test', 'test', undefined, undefined] | ||
regexp.exec("/test-test"); | ||
// => ['/test', 'test', 'test', undefined] | ||
fn("/users/123/delete"); | ||
//=> { path: '/users/123/delete', params: { id: '123' } } | ||
``` | ||
#### Modifiers | ||
## Match | ||
Modifiers are used after parameters with custom prefixes and suffixes (`{}`). | ||
The `match` function returns a function for matching strings against a path: | ||
##### Optional | ||
- **path** String or array of strings. | ||
- **options** _(optional)_ (See [parse](#parse) for more options) | ||
- **sensitive** Regexp will be case sensitive. (default: `false`) | ||
- **end** Validate the match reaches the end of the string. (default: `true`) | ||
- **trailing** Allows optional trailing delimiter to match. (default: `true`) | ||
- **decode** Function for decoding strings to params, or `false` to disable all processing. (default: `decodeURIComponent`) | ||
Parameters can be suffixed with a question mark (`?`) to make the parameter optional. | ||
```js | ||
const regexp = pathToRegexp("/:foo{/:bar}?"); | ||
// keys = [{ name: 'foo', ... }, { name: 'bar', prefix: '/', modifier: '?' }] | ||
regexp.exec("/test"); | ||
//=> [ '/test', 'test', undefined, index: 0 ] | ||
regexp.exec("/test/route"); | ||
//=> [ '/test/route', 'test', 'route', index: 0 ] | ||
const fn = match("/foo/:bar"); | ||
``` | ||
##### Zero or more | ||
**Please note:** `path-to-regexp` is intended for ordered data (e.g. pathnames, hostnames). It can not handle arbitrarily ordered data (e.g. query strings, URL fragments, JSON, etc). | ||
Parameters can be suffixed with an asterisk (`*`) to denote a zero or more parameter matches. | ||
## Compile ("Reverse" Path-To-RegExp) | ||
```js | ||
const regexp = pathToRegexp("{/:foo}*"); | ||
// keys = [{ name: 'foo', prefix: '/', modifier: '*' }] | ||
regexp.exec("/foo"); | ||
//=> [ '/foo', "foo", index: 0 ] | ||
regexp.exec("/bar/baz"); | ||
//=> [ '/bar/baz', 'bar/baz', index: 0 ] | ||
``` | ||
##### One or more | ||
Parameters can be suffixed with a plus sign (`+`) to denote a one or more parameter matches. | ||
```js | ||
const regexp = pathToRegexp("{/:foo}+"); | ||
// keys = [{ name: 'foo', prefix: '/', modifier: '+' }] | ||
regexp.exec("/"); | ||
//=> null | ||
regexp.exec("/bar/baz"); | ||
//=> [ '/bar/baz', 'bar/baz', index: 0 ] | ||
``` | ||
##### Custom separator | ||
By default, parameters set the separator as the `prefix + suffix` of the token. Using `;` you can modify this: | ||
```js | ||
const regexp = pathToRegexp("/name{/:parts;-}+"); | ||
regexp.exec("/name"); | ||
//=> null | ||
regexp.exec("/bar/1-2-3"); | ||
//=> [ '/name/1-2-3', '1-2-3', index: 0 ] | ||
``` | ||
#### Wildcard | ||
A wildcard can also be used. It is roughly equivalent to `(.*)`. | ||
```js | ||
const regexp = pathToRegexp("/*"); | ||
// keys = [{ name: '0', pattern: '[^\\/]*', separator: '/', modifier: '*' }] | ||
regexp.exec("/"); | ||
//=> [ '/', '', index: 0 ] | ||
regexp.exec("/bar/baz"); | ||
//=> [ '/bar/baz', 'bar/baz', index: 0 ] | ||
``` | ||
### Match | ||
The `match` function returns a function for transforming paths into parameters: | ||
- **path** A string. | ||
- **options** _(optional)_ The same options as `pathToRegexp`, plus: | ||
- **decode** Function for decoding strings for params, or `false` to disable entirely. (default: `decodeURIComponent`) | ||
```js | ||
const fn = match("/user/:id"); | ||
fn("/user/123"); //=> { path: '/user/123', index: 0, params: { id: '123' } } | ||
fn("/invalid"); //=> false | ||
fn("/user/caf%C3%A9"); //=> { path: '/user/caf%C3%A9', index: 0, params: { id: 'café' } } | ||
``` | ||
**Note:** Setting `decode: false` disables the "splitting" behavior of repeated parameters, which is useful if you need the exactly matched parameter back. | ||
### Compile ("Reverse" Path-To-RegExp) | ||
The `compile` function will return a function for transforming parameters into a valid path: | ||
- **path** A string. | ||
- **options** _(optional)_ Similar to `pathToRegexp` (`delimiter`, `encodePath`, `sensitive`, and `loose`), plus: | ||
- **validate** When `false` the function can produce an invalid (unmatched) path. (default: `true`) | ||
- **options** (See [parse](#parse) for more options) | ||
- **encode** Function for encoding input strings for output into the path, or `false` to disable entirely. (default: `encodeURIComponent`) | ||
@@ -231,16 +95,11 @@ | ||
// When disabling `encode`, you need to make sure inputs are encoded correctly. No arrays are accepted. | ||
const toPathRaw = compile("/user/:id", { encode: false }); | ||
const toPathRepeated = compile("/*segment"); | ||
toPathRaw({ id: "%3A%2F" }); //=> "/user/%3A%2F" | ||
toPathRaw({ id: ":/" }); //=> Throws, "/user/:/" when `validate` is `false`. | ||
const toPathRepeated = compile("{/:segment}+"); | ||
toPathRepeated({ segment: ["foo"] }); //=> "/foo" | ||
toPathRepeated({ segment: ["a", "b", "c"] }); //=> "/a/b/c" | ||
const toPathRegexp = compile("/user/:id(\\d+)"); | ||
// When disabling `encode`, you need to make sure inputs are encoded correctly. No arrays are accepted. | ||
const toPathRaw = compile("/user/:id", { encode: false }); | ||
toPathRegexp({ id: "123" }); //=> "/user/123" | ||
toPathRaw({ id: "%3A%2F" }); //=> "/user/%3A%2F" | ||
``` | ||
@@ -250,25 +109,21 @@ | ||
- If you are rewriting paths with match and compiler, consider using `encode: false` and `decode: false` to keep raw paths passed around. | ||
- To ensure matches work on paths containing characters usually encoded, consider using [encodeurl](https://github.com/pillarjs/encodeurl) for `encodePath`. | ||
- If matches are intended to be exact, you need to set `loose: false`, `trailing: false`, and `sensitive: true`. | ||
- Enable `strict: true` to detect ReDOS issues. | ||
- If you are rewriting paths with match and compile, consider using `encode: false` and `decode: false` to keep raw paths passed around. | ||
- To ensure matches work on paths containing characters usually encoded, such as emoji, consider using [encodeurl](https://github.com/pillarjs/encodeurl) for `encodePath`. | ||
### Parse | ||
A `parse` function is available and returns `TokenData`, the set of tokens and other metadata parsed from the input string. `TokenData` is can passed directly into `pathToRegexp`, `match`, and `compile`. It accepts only two options, `delimiter` and `encodePath`, which makes those options redundant in the above methods. | ||
The `parse` function accepts a string and returns `TokenData`, the set of tokens and other metadata parsed from the input string. `TokenData` is can used with `match` and `compile`. | ||
- **path** A string. | ||
- **options** _(optional)_ | ||
- **delimiter** The default delimiter for segments, e.g. `[^/]` for `:named` parameters. (default: `'/'`) | ||
- **encodePath** A function for encoding input strings. (default: `x => x`, recommended: [`encodeurl`](https://github.com/pillarjs/encodeurl)) | ||
### Tokens | ||
The `tokens` returned by `TokenData` is an array of strings or keys, represented as objects, with the following properties: | ||
`TokenData` is a sequence of tokens, currently of types `text`, `parameter`, `wildcard`, or `group`. | ||
- `name` The name of the token | ||
- `prefix` _(optional)_ The prefix string for the segment (e.g. `"/"`) | ||
- `suffix` _(optional)_ The suffix string for the segment (e.g. `""`) | ||
- `pattern` _(optional)_ The pattern defined to match this token | ||
- `modifier` _(optional)_ The modifier character used for the segment (e.g. `?`) | ||
- `separator` _(optional)_ The string used to separate repeated parameters | ||
### Custom path | ||
In some applications, you may not be able to use the `path-to-regexp` syntax (e.g. file-based routing), but you can still use this library for `match`, `compile`, and `pathToRegexp` by building your own `TokenData` instance. For example: | ||
In some applications, you may not be able to use the `path-to-regexp` syntax, but still want to use this library for `match` and `compile`. For example: | ||
@@ -278,4 +133,7 @@ ```js | ||
const tokens = ["/", { name: "foo" }]; | ||
const path = new TokenData(tokens, "/"); | ||
const tokens = [ | ||
{ type: "text", value: "/" }, | ||
{ type: "parameter", name: "foo" }, | ||
]; | ||
const path = new TokenData(tokens); | ||
const fn = match(path); | ||
@@ -290,18 +148,22 @@ | ||
### Unexpected `?`, `*`, or `+` | ||
### Unexpected `?` or `+` | ||
In previous major versions `/` and `.` were used as implicit prefixes of parameters. So `/:key?` was implicitly `{/:key}?`. For example: | ||
In past releases, `?`, `*`, and `+` were used to denote optional or repeating parameters. As an alternative, try these: | ||
- `/:key?` → `{/:key}?` or `/:key*` → `{/:key}*` or `/:key+` → `{/:key}+` | ||
- `.:key?` → `{.:key}?` or `.:key*` → `{.:key}*` or `.:key+` → `{.:key}+` | ||
- `:key?` → `{:key}?` or `:key*` → `{:key}*` or `:key+` → `{:key}+` | ||
- For optional (`?`), use an empty segment in a group such as `/:file{.:ext}`. | ||
- For repeating (`+`), only wildcard matching is supported, such as `/*path`. | ||
- For optional repeating (`*`), use a group and a wildcard parameter such as `/files{/*path}`. | ||
### Unexpected `;` | ||
### Unexpected `(`, `)`, `[`, `]`, etc. | ||
Used as a [custom separator](#custom-separator) for repeated parameters. | ||
Previous versions of Path-to-RegExp used these for RegExp features. This version no longer supports them so they've been reserved to avoid ambiguity. To use these characters literally, escape them with a backslash, e.g. `"\\("`. | ||
### Unexpected `!`, `@`, or `,` | ||
### Missing parameter name | ||
These characters have been reserved for future use. | ||
Parameter names, the part after `:` or `*`, must be a valid JavaScript identifier. For example, it cannot start with a number or contain a dash. If you want a parameter name that uses these characters you can wrap the name in quotes, e.g. `:"my-name"`. | ||
### Unterminated quote | ||
Parameter names can be wrapped in double quote characters, and this error means you forgot to close the quote character. | ||
### Express <= 4.x | ||
@@ -311,7 +173,6 @@ | ||
- The only part of the string that is a regex is within `()`. | ||
- In Express.js 4.x, everything was passed as-is after a simple replacement, so you could write `/[a-z]+` to match `/test`. | ||
- The `?` optional character must be used after `{}`. | ||
- Regexp characters can no longer be provided. | ||
- The optional character `?` is no longer supported, use braces instead: `/:file{.:ext}`. | ||
- Some characters have new meaning or have been reserved (`{}?*+@!;`). | ||
- The parameter name now supports all unicode identifier characters, previously it was only `[a-z0-9]`. | ||
- The parameter name now supports all JavaScript identifier characters, previously it was only `[a-z0-9]`. | ||
@@ -318,0 +179,0 @@ ## License |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
51290
493
187
1