From d6150f5f27e67fcdc532e556a51531e8bdaa498d Mon Sep 17 00:00:00 2001 From: Blake Embrey Date: Mon, 9 Sep 2024 16:59:42 -0700 Subject: [PATCH] Add pathToRegexp method back --- Readme.md | 25 +++++--- scripts/redos.ts | 10 +-- src/cases.spec.ts | 6 +- src/index.ts | 158 ++++++++++++++++++++++------------------------ 4 files changed, 101 insertions(+), 98 deletions(-) diff --git a/Readme.md b/Readme.md index 902fe90..3c24e66 100644 --- a/Readme.md +++ b/Readme.md @@ -17,11 +17,7 @@ npm install path-to-regexp --save ## Usage ```js -const { match, compile, parse } = require("path-to-regexp"); - -// match(path, options?) -// compile(path, options?) -// parse(path, options?) +const { match, pathToRegexp, compile, parse } = require("path-to-regexp"); ``` ### Parameters @@ -64,20 +60,31 @@ fn("/users/123/delete"); The `match` function returns a function for matching strings against a path: +- **path** String or array of strings. +- **options** _(optional)_ (Extends [pathToRegexp](#pathToRegexp) options) + - **decode** Function for decoding strings to params, or `false` to disable all processing. (default: `decodeURIComponent`) + +```js +const fn = match("/foo/:bar"); +``` + +**Please note:** `path-to-regexp` is intended for ordered data (e.g. paths, hosts). It can not handle arbitrarily ordered data (e.g. query strings, URL fragments, JSON, etc). + +## PathToRegexp + +The `pathToRegexp` function returns a regular expression for matching strings against paths. It + - **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`) - **delimiter** The default delimiter for segments, e.g. `[^/]` for `:named` parameters. (default: `'/'`) - **trailing** Allows optional trailing delimiter to match. (default: `true`) - - **decode** Function for decoding strings to params, or `false` to disable all processing. (default: `decodeURIComponent`) ```js -const fn = match("/foo/:bar"); +const { regexp, keys } = pathToRegexp("/foo/:bar"); ``` -**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). - ## Compile ("Reverse" Path-To-RegExp) The `compile` function will return a function for transforming parameters into a valid path: diff --git a/scripts/redos.ts b/scripts/redos.ts index 841cd07..9f0b4bc 100644 --- a/scripts/redos.ts +++ b/scripts/redos.ts @@ -1,5 +1,5 @@ import { checkSync } from "recheck"; -import { match } from "../src/index.js"; +import { pathToRegexp } from "../src/index.js"; import { MATCH_TESTS } from "../src/cases.spec.js"; let safe = 0; @@ -8,14 +8,14 @@ let fail = 0; const TESTS = MATCH_TESTS.map((x) => x.path); for (const path of TESTS) { - const { re } = match(path) as any; - const result = checkSync(re.source, re.flags); + const { regexp } = pathToRegexp(path); + const result = checkSync(regexp.source, regexp.flags); if (result.status === "safe") { safe++; - console.log("Safe:", path, String(re)); + console.log("Safe:", path, String(regexp)); } else { fail++; - console.log("Fail:", path, String(re)); + console.log("Fail:", path, String(regexp)); } } diff --git a/src/cases.spec.ts b/src/cases.spec.ts index dee3f29..6a7aeec 100644 --- a/src/cases.spec.ts +++ b/src/cases.spec.ts @@ -302,6 +302,10 @@ export const MATCH_TESTS: MatchTestSet[] = [ input: "/test/", expected: { path: "/test/", params: {} }, }, + { + input: "/TEST/", + expected: { path: "/TEST/", params: {} }, + }, ], }, { @@ -394,11 +398,11 @@ export const MATCH_TESTS: MatchTestSet[] = [ sensitive: true, }, tests: [ + { input: "/test", expected: false }, { input: "/TEST", expected: { path: "/TEST", params: {} }, }, - { input: "/test", expected: false }, ], }, diff --git a/src/index.ts b/src/index.ts index 2c5a088..5a0d326 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,11 +21,7 @@ export interface ParseOptions { encodePath?: Encode; } -export interface MatchOptions { - /** - * Function for decoding strings for params, or `false` to disable entirely. (default: `decodeURIComponent`) - */ - decode?: Decode | false; +export interface PathToRegexpOptions { /** * Matches the path completely without trailing characters. (default: `true`) */ @@ -44,6 +40,13 @@ export interface MatchOptions { delimiter?: string; } +export interface MatchOptions extends PathToRegexpOptions { + /** + * Function for decoding strings for params, or `false` to disable entirely. (default: `decodeURIComponent`) + */ + decode?: Decode | false; +} + export interface CompileOptions { /** * Function for encoding input strings for output into the path, or `false` to disable entirely. (default: `encodeURIComponent`) @@ -109,13 +112,6 @@ function escape(str: string) { return str.replace(/[.+*?^${}()[\]|/\\]/g, "\\$&"); } -/** - * Get the flags for a regexp from the options. - */ -function toFlags(options: { sensitive?: boolean }) { - return options.sensitive ? "s" : "is"; -} - /** * Tokenize input string. */ @@ -253,6 +249,16 @@ export interface Group { tokens: Token[]; } +/** + * A token that corresponds with a regexp capture. + */ +export type Key = Parameter | Wildcard; + +/** + * A sequence of `path-to-regexp` keys that match capturing groups. + */ +export type Keys = Array; + /** * A sequence of path match characters. */ @@ -316,14 +322,15 @@ export function parse(str: string, options: ParseOptions = {}): TokenData { } /** - * Transform tokens into a path building function. + * Compile a string to a template function for the path. */ -function $compile

( - data: TokenData, - options: CompileOptions, -): PathFunction

{ +export function compile

( + path: Path, + options: CompileOptions & ParseOptions = {}, +) { 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: P = {} as P) { @@ -335,19 +342,6 @@ function $compile

( }; } -/** - * Compile a string to a template function for the path. - */ -export function compile

( - path: Path, - options: CompileOptions & ParseOptions = {}, -) { - return $compile

( - path instanceof TokenData ? path : parse(path, options), - options, - ); -} - export type ParamData = Partial>; export type PathFunction

= (data?: P) => string; @@ -451,34 +445,20 @@ export type Match

= false | MatchResult

; export type MatchFunction

= (path: string) => Match

; /** - * Create path match function from `path-to-regexp` spec. + * Supported path types. */ -function $match

( - data: TokenData[], - options: MatchOptions = {}, -): MatchFunction

{ - const { - decode = decodeURIComponent, - delimiter = DEFAULT_DELIMITER, - end = true, - trailing = true, - } = options; - const flags = toFlags(options); - const sources: string[] = []; - const keys: Array = []; - - 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)}|$)`; +export type Path = string | TokenData; - const re = new RegExp(pattern, flags); +/** + * Transform a path into a match function. + */ +export function match

( + path: Path | Path[], + options: MatchOptions & ParseOptions = {}, +): MatchFunction

{ + const { decode = decodeURIComponent, delimiter = DEFAULT_DELIMITER } = + options; + const { regexp, keys } = pathToRegexp(path, options); const decoders = keys.map((key) => { if (decode === false) return NOOP_VALUE; @@ -486,40 +466,56 @@ function $match

( return (value: string) => value.split(delimiter).map(decode); }); - return Object.assign( - function match(input: string) { - const m = re.exec(input); - if (!m) return false; + return function match(input: string) { + const m = regexp.exec(input); + if (!m) return false; - const { 0: path } = m; - const params = Object.create(null); + const path = m[0]; + const params = Object.create(null); - for (let i = 1; i < m.length; i++) { - if (m[i] === undefined) continue; + 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]); - } + const key = keys[i - 1]; + const decoder = decoders[i - 1]; + params[key.name] = decoder(m[i]); + } - return { path, params }; - }, - { re }, - ); + return { path, params }; + }; } -export type Path = string | TokenData; - -export function match

( +export function pathToRegexp( path: Path | Path[], - options: MatchOptions & ParseOptions = {}, -): MatchFunction

{ + options: PathToRegexpOptions & ParseOptions = {}, +) { + const { + delimiter = DEFAULT_DELIMITER, + end = true, + sensitive = false, + trailing = true, + } = options; + const keys: Keys = []; + const sources: string[] = []; + const flags = sensitive ? "s" : "is"; const paths = Array.isArray(path) ? path : [path]; const items = paths.map((path) => path instanceof TokenData ? path : parse(path, options), ); - return $match(items, 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 }; } /** @@ -556,11 +552,7 @@ function* flatten( /** * Transform a flat sequence of tokens into a regular expression. */ -function sequenceToRegExp( - tokens: Flattened[], - delimiter: string, - keys: Array, -): string { +function sequenceToRegExp(tokens: Flattened[], delimiter: string, keys: Keys) { let result = ""; let backtrack = ""; let isSafeSegmentParam = true;