@@ -11,9 +11,14 @@ import (
1111 "reflect"
1212 "strconv"
1313 "strings"
14+ "unicode"
1415 "unsafe"
1516
17+ "golang.org/x/text/runes"
18+
1619 "github.com/dlclark/regexp2"
20+ "golang.org/x/text/transform"
21+ "golang.org/x/text/unicode/norm"
1722)
1823
1924// Token is parsed from path. For example, using `/user/:id`, `tokens` will
@@ -88,6 +93,53 @@ const defaultDelimiter = "/"
8893var escapeRegexp = regexp2 .MustCompile ("([.+*?=^!:${}()[\\ ]|/\\ \\ ])" , regexp2 .None )
8994var tokenRegexp = regexp2 .MustCompile ("\\ ((?!\\ ?)" , regexp2 .None )
9095
96+ func normalize (str string ) string {
97+ t := transform .Chain (norm .NFC , runes .Remove (runes .In (unicode .Mn )), norm .NFC )
98+ normStr , _ , _ := transform .String (t , str )
99+ return normStr
100+ }
101+
102+ // NormalizePathname normalizes a pathname for matching, replaces multiple slashes
103+ // with a single slash and normalizes unicode characters to "NFC". When using this method,
104+ // `decode` should be an identity function so you don't decode strings twice.
105+ func NormalizePathname (pathname string ) string {
106+ r := regexp2 .MustCompile ("\\ /+" , regexp2 .None )
107+ str , err := r .Replace (DecodeURIComponent (pathname , nil ),
108+ "/" , - 1 , - 1 )
109+ if err != nil {
110+ panic (err )
111+ }
112+ return normalize (str )
113+ }
114+
115+ // Balanced bracket helper function.
116+ func balanced (open string , close string , str string , index int ) int {
117+ count , i , arr := 0 , index , strings .Split (str , "" )
118+
119+ for i < len (arr ) {
120+ if arr [i ] == "\\ " {
121+ i += 2
122+ continue
123+ }
124+
125+ if arr [i ] == close {
126+ count --
127+
128+ if count == 0 {
129+ return i + 1
130+ }
131+ }
132+
133+ if arr [i ] == open {
134+ count ++
135+ }
136+
137+ i ++
138+ }
139+
140+ return - 1
141+ }
142+
91143// Parse a string for the raw tokens.
92144func Parse (str string , o * Options ) []interface {} {
93145 tokens , tokenIndex , index , path , isEscaped := make ([]interface {}, 0 ), 0 , 0 , "" , false
@@ -136,49 +188,27 @@ func Parse(str string, o *Options) []interface{} {
136188 }
137189
138190 if index < length && arr [index ] == "(" {
139- prev , balanced , invalidGroup := index , 1 , false
140- if index + 1 < length && arr [index + 1 ] == "?" {
141- panic ("Path pattern must be a capturing group" )
142- }
143-
144- for index ++ ; index < length ; index ++ {
145- if arr [index ] == "\\ " {
146- pattern += strings .Join (arr [index :min (index + 2 , length )], "" )
147- index ++
148- continue
191+ end := balanced ("(" , ")" , str , index )
192+
193+ // False positive on matching brackets.
194+ if end > - 1 {
195+ pattern = strings .Join (arr [index + 1 :end - 1 ], "" )
196+ index = end
197+ if pattern [0 ] == '?' {
198+ panic ("Path pattern must be a capturing group" )
149199 }
150200
151- if arr [ index ] == ")" {
152- balanced --
153- if balanced == 0 {
154- index ++
155- break
201+ r := regexp2 . MustCompile ( " \\ ((?=[^?])" , regexp2 . None )
202+ if ok , _ := r . MatchString ( pattern ); ok {
203+ validPattern , err := r . Replace ( pattern , "(?:" , - 1 , - 1 )
204+ if err != nil {
205+ panic ( err )
156206 }
157- }
158207
159- pattern += string (arr [index ])
160-
161- if arr [index ] == "(" {
162- balanced ++
163-
164- // Better errors on nested capturing groups.
165- if index + 1 >= length || arr [index + 1 ] != "?" {
166- pattern += "?:"
167- invalidGroup = true
168- }
208+ panic (fmt .Sprintf ("Capturing groups are not allowed in pattern, " +
209+ "use a non-capturing group: (%s)" , validPattern ))
169210 }
170211 }
171-
172- if invalidGroup {
173- panic (fmt .Sprintf ("Capturing groups are not allowed in pattern, " +
174- "use a non-capturing group: (%s)" , pattern ))
175- }
176-
177- // False positive.
178- if balanced > 0 {
179- index = prev
180- pattern = ""
181- }
182212 }
183213
184214 // Add regular characters to the path string.
@@ -271,7 +301,7 @@ func Match(path interface{}, o *Options) (func(string) *MatchResult, error) {
271301 return nil , err
272302 }
273303
274- return regexpToFunction (re , tokens ), nil
304+ return regexpToFunction (re , tokens , o ), nil
275305}
276306
277307// MustMatch is like Match but panics if err occur in match function.
@@ -284,8 +314,13 @@ func MustMatch(path interface{}, o *Options) func(string) *MatchResult {
284314}
285315
286316// Create a path match function from `path-to-regexp` output.
287- func regexpToFunction (re * regexp2.Regexp , tokens []Token ) func (string ) * MatchResult {
288- decode := DecodeURIComponent
317+ func regexpToFunction (re * regexp2.Regexp , tokens []Token , o * Options ) func (string ) * MatchResult {
318+ decode := func (str string , token interface {}) string {
319+ return str
320+ }
321+ if o != nil && o .Decode != nil {
322+ decode = o .Decode
323+ }
289324
290325 return func (pathname string ) * MatchResult {
291326 m , err := re .FindStringMatch (pathname )
@@ -513,6 +548,20 @@ func toMap(data interface{}) map[interface{}]interface{} {
513548 return m
514549}
515550
551+ func EncodeURI (str string ) string {
552+ excludes := ";/?:@&=+$,#"
553+ arr := strings .Split (str , "" )
554+ result := ""
555+ for _ , v := range arr {
556+ if strings .Contains (excludes , v ) {
557+ result += v
558+ } else {
559+ result += EncodeURIComponent (v , nil )
560+ }
561+ }
562+ return result
563+ }
564+
516565func EncodeURIComponent (str string , token interface {}) string {
517566 r := url .QueryEscape (str )
518567 r = strings .Replace (r , "+" , "%20" , - 1 )
0 commit comments