@@ -63,14 +63,14 @@ import SwiftUI
63
63
///
64
64
/// - Note: A `Route`'s default path is `*`, meaning it will always match.
65
65
public struct Route < ValidatedData, Content: View > : View {
66
-
66
+
67
67
public typealias Validator = ( RouteInformation ) -> ValidatedData ?
68
68
69
69
@Environment ( \. relativePath) private var relativePath
70
70
@EnvironmentObject private var navigator : Navigator
71
71
@EnvironmentObject private var switchEnvironment : SwitchRoutesEnvironment
72
72
@StateObject private var pathMatcher = PathMatcher ( )
73
-
73
+
74
74
private let content : ( ValidatedData ) -> Content
75
75
private let path : String
76
76
private let validator : Validator
@@ -111,7 +111,7 @@ public struct Route<ValidatedData, Content: View>: View {
111
111
{
112
112
validatedData = validated
113
113
routeInformation = matchInformation
114
-
114
+
115
115
if switchEnvironment. isActive {
116
116
switchEnvironment. isResolved = true
117
117
}
@@ -143,15 +143,15 @@ public extension Route where ValidatedData == RouteInformation {
143
143
self . validator = { $0 }
144
144
self . content = content
145
145
}
146
-
146
+
147
147
/// - Parameter path: A path glob to test with the current path. See documentation for `Route`.
148
148
/// - Parameter content: Views to render.
149
149
init ( _ path: String = " * " , @ViewBuilder content: @escaping ( ) -> Content ) {
150
150
self . path = path
151
151
self . validator = { $0 }
152
152
self . content = { _ in content ( ) }
153
153
}
154
-
154
+
155
155
/// - Parameter path: A path glob to test with the current path. See documentation for `Route`.
156
156
/// - Parameter content: View to render (autoclosure).
157
157
init ( _ path: String = " * " , content: @autoclosure @escaping ( ) -> Content ) {
@@ -190,13 +190,13 @@ public extension Route where ValidatedData == RouteInformation {
190
190
public final class RouteInformation : ObservableObject {
191
191
/// The resolved path component of the parent `Route`. For internal use only, at the moment.
192
192
let matchedPath : String
193
-
193
+
194
194
/// The current relative path.
195
195
public let path : String
196
196
197
197
/// Resolved parameters of the parent `Route`s path.
198
198
public let parameters : [ String : String ]
199
-
199
+
200
200
init ( path: String , matchedPath: String , parameters: [ String : String ] = [ : ] ) {
201
201
self . matchedPath = matchedPath
202
202
self . parameters = parameters
@@ -215,11 +215,11 @@ final class PathMatcher: ObservableObject {
215
215
let matchRegex : NSRegularExpression
216
216
let parameters : Set < String >
217
217
}
218
-
218
+
219
219
private enum CompileError : Error {
220
220
case badParameter( String , culprit: String )
221
221
}
222
-
222
+
223
223
private static let variablesRegex = try ! NSRegularExpression ( pattern: #":([^\/\?]+)"# , options: [ ] )
224
224
225
225
//
@@ -261,19 +261,21 @@ final class PathMatcher: ObservableObject {
261
261
var pattern = glob
262
262
. replacingOccurrences ( of: " ^[^/]/$ " , with: " " , options: . regularExpression) // Trailing slash.
263
263
. replacingOccurrences ( of: #"\/?\*"# , with: " " , options: . regularExpression) // Trailing asterisk.
264
-
265
- for variable in variables {
264
+
265
+ for (index, variable) in variables. enumerated ( ) {
266
+ let isAtRoot = index == 0 && glob. starts ( with: " /: " + variable)
266
267
pattern = pattern. replacingOccurrences (
267
268
of: " /: " + variable,
268
- with: " / (?<" + variable + " > [^/?]+)" , // Named capture group.
269
+ with: ( isAtRoot ? " / " : " " ) + " (?< \( variable ) > " + ( isAtRoot ? " " : " /? " ) + " [^/?]+) " ,
269
270
options: . regularExpression)
270
271
}
272
+
271
273
pattern = " ^ " +
272
274
( pattern. isEmpty ? " " : " ( \( pattern) ) " ) +
273
275
( endsWithAsterisk ? " (/.*)?$ " : " $ " )
274
276
275
277
let regex = try NSRegularExpression ( pattern: pattern, options: [ ] )
276
-
278
+
277
279
cached = CompiledRegex ( path: glob, matchRegex: regex, parameters: variables)
278
280
279
281
return cached!
@@ -288,7 +290,7 @@ final class PathMatcher: ObservableObject {
288
290
if matches. isEmpty {
289
291
return nil
290
292
}
291
-
293
+
292
294
var parameterValues : [ String : String ] = [ : ]
293
295
294
296
if !compiled. parameters. isEmpty {
@@ -297,11 +299,17 @@ final class PathMatcher: ObservableObject {
297
299
if nsrange. location != NSNotFound,
298
300
let range = Range ( nsrange, in: path)
299
301
{
300
- parameterValues [ variable] = String ( path [ range] )
302
+ var value = String ( path [ range] )
303
+
304
+ if value. starts ( with: " / " ) {
305
+ value = String ( value. dropFirst ( ) )
306
+ }
307
+
308
+ parameterValues [ variable] = value
301
309
}
302
310
}
303
311
}
304
-
312
+
305
313
// Resolve the glob to get a new relative path.
306
314
// We only want the part the glob is directly referencing.
307
315
// I.e., if the glob is `/news/article/*` and the navigation path is `/news/article/1/details`,
@@ -312,7 +320,7 @@ final class PathMatcher: ObservableObject {
312
320
else {
313
321
return nil
314
322
}
315
-
323
+
316
324
let resolvedGlob = String ( path [ range] )
317
325
let matchedPath = String ( path [ relative. endIndex... ] )
318
326
0 commit comments