33 */
44const DEFAULT_DELIMITER = "/" ;
55
6- /**
7- * The main path matching regexp utility.
8- */
9- const PATH_REGEXP = new RegExp (
10- [
11- // Match escaped characters that would otherwise appear in future matches.
12- // This allows the user to escape special characters that won't transform.
13- "(\\\\.)" ,
14- // Match Express-style parameters and un-named parameters with a prefix
15- // and optional suffixes. Matches appear as:
16- //
17- // ":test(\\d+)?" => ["test", "\d+", undefined, "?"]
18- // "(\\d+)" => [undefined, undefined, "\d+", undefined]
19- "(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?"
20- ] . join ( "|" ) ,
21- "g"
22- ) ;
23-
246export interface ParseOptions {
257 /**
268 * Set the default delimiter for repeat parameters. (default: `'/'`)
@@ -39,70 +21,149 @@ export function parse(str: string, options: ParseOptions = {}): Token[] {
3921 const tokens = [ ] ;
4022 const defaultDelimiter = options . delimiter ?? DEFAULT_DELIMITER ;
4123 const whitelist = options . whitelist ?? undefined ;
24+ let i = 0 ;
4225 let key = 0 ;
43- let index = 0 ;
4426 let path = "" ;
45- let isPathEscaped = false ;
46- let res : RegExpExecArray | null ;
27+ let isEscaped = false ;
4728
4829 // tslint:disable-next-line
49- while ( ( res = PATH_REGEXP . exec ( str ) ) !== null ) {
50- const [ m , escaped , name , capture , group , modifier ] = res ;
51- let prev = "" ;
30+ while ( i < str . length ) {
31+ let prefix = "" ;
32+ let name = "" ;
33+ let pattern = "" ;
34+
35+ // Ignore escaped sequences.
36+ if ( str [ i ] === "\\" ) {
37+ i ++ ;
38+ path += str [ i ++ ] ;
39+ isEscaped = true ;
40+ continue ;
41+ }
42+
43+ if ( str [ i ] === ":" ) {
44+ while ( ++ i < str . length ) {
45+ const code = str . charCodeAt ( i ) ;
46+
47+ if (
48+ // `0-9`
49+ ( code >= 48 && code <= 57 ) ||
50+ // `A-Z`
51+ ( code >= 65 && code <= 90 ) ||
52+ // `a-z`
53+ ( code >= 97 && code <= 122 ) ||
54+ // `_`
55+ code === 95
56+ ) {
57+ name += str [ i ] ;
58+ continue ;
59+ }
60+
61+ break ;
62+ }
63+
64+ // False positive on param name.
65+ if ( ! name ) i -- ;
66+ }
67+
68+ if ( str [ i ] === "(" ) {
69+ const prev = i ;
70+ let balanced = 1 ;
71+ let invalidGroup = false ;
72+
73+ if ( str [ i + 1 ] === "?" ) {
74+ throw new TypeError ( "Path pattern must be a capturing group" ) ;
75+ }
76+
77+ while ( ++ i < str . length ) {
78+ if ( str [ i ] === "\\" ) {
79+ pattern += str . substr ( i , 2 ) ;
80+ i ++ ;
81+ continue ;
82+ }
83+
84+ if ( str [ i ] === ")" ) {
85+ balanced -- ;
86+
87+ if ( balanced === 0 ) {
88+ i ++ ;
89+ break ;
90+ }
91+ }
92+
93+ pattern += str [ i ] ;
94+
95+ if ( str [ i ] === "(" ) {
96+ balanced ++ ;
97+
98+ // Better errors on nested capturing groups.
99+ if ( str [ i + 1 ] !== "?" ) {
100+ pattern += "?:" ;
101+ invalidGroup = true ;
102+ }
103+ }
104+ }
105+
106+ if ( invalidGroup ) {
107+ throw new TypeError (
108+ `Capturing groups are not allowed in pattern, use a non-capturing group: (${ pattern } )`
109+ ) ;
110+ }
52111
53- path += str . slice ( index , res . index ) ;
54- index = res . index + m . length ;
112+ // False positive.
113+ if ( balanced > 0 ) {
114+ i = prev ;
115+ pattern = "" ;
116+ }
117+ }
55118
56- // Ignore already escaped sequences .
57- if ( escaped ) {
58- path += escaped [ 1 ] ;
59- isPathEscaped = true ;
119+ // Add regular characters to the path string .
120+ if ( name === "" && pattern === "" ) {
121+ path += str [ i ++ ] ;
122+ isEscaped = false ;
60123 continue ;
61124 }
62125
63- if ( ! isPathEscaped && path . length ) {
64- const k = path . length - 1 ;
65- const c = path [ k ] ;
66- const matches = whitelist ? whitelist . indexOf ( c ) > - 1 : true ;
126+ // Extract the final character from ` path` for the prefix.
127+ if ( path . length && ! isEscaped ) {
128+ const char = path [ path . length - 1 ] ;
129+ const matches = whitelist ? whitelist . indexOf ( char ) > - 1 : true ;
67130
68131 if ( matches ) {
69- prev = c ;
70- path = path . slice ( 0 , k ) ;
132+ prefix = char ;
133+ path = path . slice ( 0 , - 1 ) ;
71134 }
72135 }
73136
74- // Push the current path onto the tokens.
75- if ( path ) {
137+ // Push the current path onto the list of tokens.
138+ if ( path . length ) {
76139 tokens . push ( path ) ;
77140 path = "" ;
78- isPathEscaped = false ;
79141 }
80142
81- const repeat = modifier === "+" || modifier === "*" ;
82- const optional = modifier === "?" || modifier === "*" ;
83- const pattern = capture || group ;
84- const delimiter = prev || defaultDelimiter ;
143+ const repeat = str [ i ] === "+" || str [ i ] === "*" ;
144+ const optional = str [ i ] === "?" || str [ i ] === "*" ;
145+ const delimiter = prefix || defaultDelimiter ;
146+
147+ // Increment `i` past modifier token.
148+ if ( repeat || optional ) i ++ ;
85149
86150 tokens . push ( {
87151 name : name || key ++ ,
88- prefix : prev ,
89- delimiter : delimiter ,
90- optional : optional ,
91- repeat : repeat ,
92- pattern : pattern
93- ? escapeGroup ( pattern )
94- : `[^${ escapeString (
95- delimiter === defaultDelimiter
96- ? delimiter
97- : delimiter + defaultDelimiter
98- ) } ]+?`
152+ prefix,
153+ delimiter,
154+ optional,
155+ repeat,
156+ pattern :
157+ pattern ||
158+ `[^${ escapeString (
159+ delimiter === defaultDelimiter
160+ ? delimiter
161+ : delimiter + defaultDelimiter
162+ ) } ]+?`
99163 } ) ;
100164 }
101165
102- // Push any remaining characters.
103- if ( path || index < str . length ) {
104- tokens . push ( path + str . substr ( index ) ) ;
105- }
166+ if ( path . length ) tokens . push ( path ) ;
106167
107168 return tokens ;
108169}
@@ -298,13 +359,6 @@ function escapeString(str: string) {
298359 return str . replace ( / ( [ . + * ? = ^ ! : $ { } ( ) [ \] | / \\ ] ) / g, "\\$1" ) ;
299360}
300361
301- /**
302- * Escape the capturing group by escaping special characters and meaning.
303- */
304- function escapeGroup ( group : string ) {
305- return group . replace ( / ( [ = ! : $ / ( ) ] ) / g, "\\$1" ) ;
306- }
307-
308362/**
309363 * Get the flags for a regexp from the options.
310364 */
0 commit comments