3
3
*/
4
4
const DEFAULT_DELIMITER = "/" ;
5
5
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
-
24
6
export interface ParseOptions {
25
7
/**
26
8
* Set the default delimiter for repeat parameters. (default: `'/'`)
@@ -39,70 +21,149 @@ export function parse(str: string, options: ParseOptions = {}): Token[] {
39
21
const tokens = [ ] ;
40
22
const defaultDelimiter = options . delimiter ?? DEFAULT_DELIMITER ;
41
23
const whitelist = options . whitelist ?? undefined ;
24
+ let i = 0 ;
42
25
let key = 0 ;
43
- let index = 0 ;
44
26
let path = "" ;
45
- let isPathEscaped = false ;
46
- let res : RegExpExecArray | null ;
27
+ let isEscaped = false ;
47
28
48
29
// 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
+ }
52
111
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
+ }
55
118
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 ;
60
123
continue ;
61
124
}
62
125
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 ;
67
130
68
131
if ( matches ) {
69
- prev = c ;
70
- path = path . slice ( 0 , k ) ;
132
+ prefix = char ;
133
+ path = path . slice ( 0 , - 1 ) ;
71
134
}
72
135
}
73
136
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 ) {
76
139
tokens . push ( path ) ;
77
140
path = "" ;
78
- isPathEscaped = false ;
79
141
}
80
142
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 ++ ;
85
149
86
150
tokens . push ( {
87
151
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
+ ) } ]+?`
99
163
} ) ;
100
164
}
101
165
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 ) ;
106
167
107
168
return tokens ;
108
169
}
@@ -298,13 +359,6 @@ function escapeString(str: string) {
298
359
return str . replace ( / ( [ . + * ? = ^ ! : $ { } ( ) [ \] | / \\ ] ) / g, "\\$1" ) ;
299
360
}
300
361
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
-
308
362
/**
309
363
* Get the flags for a regexp from the options.
310
364
*/
0 commit comments