Line data Source code
1 : part of '../main.dart';
2 :
3 : /// If the VRouteElement does have a page to display, it should instantiate this class
4 : ///
5 : /// What is does is:
6 : /// - Requiring attributes [path], [name], [aliases], [widget] and [mustMatchStackedRoutes]
7 : /// - Computing attributes [pathRegExp], [aliasesRegExp], [pathParametersKeys],
8 : /// [aliasesParameters] and [stateKey]
9 : /// - implementing [build] and [getPathFromName] methods for them
10 : @immutable
11 : abstract class VRouteElementWithPath extends VRouteElement {
12 : /// The path (local or absolute) or this [VRouteElement]
13 : ///
14 : /// If the path of a subroute is exactly matched, this will be used in
15 : /// the route but might be covered by another [VRouteElement.widget]
16 : /// The value of the path ca have three form:
17 : /// * starting with '/': The path will be treated as a route path,
18 : /// this is useful to take full advantage of nested routes while
19 : /// conserving the freedom of path naming
20 : /// * not starting with '/': The path corresponding to this route
21 : /// will be the path of the parent route + this path. If this is used
22 : /// directly in the [VRouter] routes, a '/' will be added anyway
23 : /// * be null: In this case this path will match the parent path
24 : ///
25 : /// Note we use the package [path_to_regexp](https://pub.dev/packages/path_to_regexp)
26 : /// so you can use naming such as /user/:id to get the id (see [VRouteElementData.pathParameters]
27 : /// You can also use more advance technique using regexp directly in your path, for example
28 : /// '.*' will match any route, '/user/:id(\d+)' will match any route starting with user
29 : /// and followed by a digit. Here is a recap:
30 : /// | pattern | matched path | [VRouteElementData.pathParameters]
31 : /// | /user/:username | /user/evan | { username: 'evan' }
32 : /// | /user/:id(\d+) | /user/123 | { id: '123' }
33 : /// | .* | every path | -
34 : // TODO: change example
35 : final String? path;
36 :
37 : /// A name for the route which will allow you to easily navigate to it
38 : /// using [VRouterData.of(context).pushNamed]
39 : ///
40 : /// Note that [name] should be unique w.r.t every [VRouteElement]
41 : final String? name;
42 :
43 : /// An alternative path that will be matched to this route
44 : final List<String> aliases;
45 :
46 : final bool mustMatchSubRoute;
47 :
48 : final List<VRouteElement> stackedRoutes;
49 :
50 12 : VRouteElementWithPath({
51 : required this.path,
52 : this.name,
53 : this.stackedRoutes = const [],
54 : this.aliases = const [],
55 : this.mustMatchSubRoute = false,
56 12 : }) : pathRegExp = (path != null) ? pathToRegExp(path, prefix: true) : null,
57 27 : aliasesRegExp = [for (var alias in aliases) pathToRegExp(alias, prefix: true)],
58 12 : pathParametersKeys = <String>[],
59 30 : aliasesPathParametersKeys = List<List<String>>.generate(aliases.length, (_) => []) {
60 : // Get local parameters
61 12 : if (path != null) {
62 52 : final localPath = path!.startsWith('/') ? path!.substring(1) : path!;
63 24 : pathToRegExp(localPath, parameters: pathParametersKeys);
64 : }
65 :
66 39 : for (var i = 0; i < aliases.length; i++) {
67 6 : final alias = aliases[i];
68 9 : final localPath = alias[i].startsWith('/') ? alias.substring(1) : alias;
69 9 : pathToRegExp(localPath, parameters: aliasesPathParametersKeys[i]);
70 : }
71 : }
72 :
73 : // /// A key for the [VBasePage] that will host the widget
74 : // /// You shouldn't care about using it unless you don't
75 : // /// specify a path.
76 : // LocalKey? get key;
77 :
78 : /// RegExp version of the path
79 : /// It is created automatically
80 : /// If the path starts with '/', it is removed from
81 : /// this regExp.
82 : final RegExp? pathRegExp;
83 :
84 : /// RegExp version of the aliases
85 : /// It is created automatically
86 : /// If an alias starts with '/', it is removed from
87 : /// this regExp.
88 : final List<RegExp> aliasesRegExp;
89 :
90 : /// Parameters of the path
91 : /// It is created automatically
92 : final List<String> pathParametersKeys;
93 :
94 : /// Parameters of the aliases if any
95 : /// It is created automatically
96 : final List<List<String>> aliasesPathParametersKeys;
97 :
98 : /// [entirePath] is the entire path given (in push for example)
99 : ///
100 : /// [parentRemainingPath] is the part of the path which is left to match
101 : /// after the parent [VRouteElement] matched the [entirePath]
102 : /// WARNING: [parentRemainingPath] is null if the parent did not match the path
103 : /// in which case only absolute path should be tested.
104 10 : VRoute? buildRoute(
105 : VPathRequestData vPathRequestData, {
106 : required String? parentRemainingPath,
107 : required Map<String, String> parentPathParameters,
108 : }) {
109 : // This will hold the GetPathMatchResult for the path so that we compute it only once
110 : late final GetPathMatchResult pathGetPathMatchResult;
111 :
112 : // This will hold every GetPathMatchResult for the aliases so that we compute them only once
113 10 : List<GetPathMatchResult> aliasesGetPathMatchResult = [];
114 :
115 : // Try to find valid VRoute from stackedRoutes
116 :
117 : // Check for the path
118 10 : pathGetPathMatchResult = getPathMatch(
119 10 : entirePath: vPathRequestData.path,
120 : remainingPathFromParent: parentRemainingPath,
121 10 : selfPath: path,
122 10 : selfPathRegExp: pathRegExp,
123 10 : selfPathParametersKeys: pathParametersKeys,
124 : parentPathParameters: parentPathParameters,
125 : );
126 10 : final VRoute? stackedRouteVRoute = getVRouteFromRoutes(
127 : vPathRequestData,
128 10 : routes: stackedRoutes,
129 0 : getPathMatchResult: pathGetPathMatchResult,
130 : );
131 : if (stackedRouteVRoute != null) {
132 10 : return VRoute(
133 10 : vRouteElementNode: VRouteElementNode(
134 : this,
135 10 : stackedVRouteElementNode: stackedRouteVRoute.vRouteElementNode,
136 : ),
137 10 : pages: stackedRouteVRoute.pages,
138 10 : pathParameters: stackedRouteVRoute.pathParameters,
139 30 : vRouteElements: <VRouteElement>[this] + stackedRouteVRoute.vRouteElements,
140 : );
141 : }
142 :
143 : // Check for the aliases
144 31 : for (var i = 0; i < aliases.length; i++) {
145 1 : aliasesGetPathMatchResult.add(
146 1 : getPathMatch(
147 1 : entirePath: vPathRequestData.path,
148 : remainingPathFromParent: parentRemainingPath,
149 2 : selfPath: aliases[i],
150 2 : selfPathRegExp: aliasesRegExp[i],
151 2 : selfPathParametersKeys: aliasesPathParametersKeys[i],
152 : parentPathParameters: parentPathParameters,
153 : ),
154 : );
155 1 : final VRoute? stackedRouteVRoute = getVRouteFromRoutes(
156 : vPathRequestData,
157 1 : routes: stackedRoutes,
158 1 : getPathMatchResult: aliasesGetPathMatchResult[i],
159 : );
160 : if (stackedRouteVRoute != null) {
161 0 : return VRoute(
162 0 : vRouteElementNode: VRouteElementNode(
163 : this,
164 0 : stackedVRouteElementNode: stackedRouteVRoute.vRouteElementNode,
165 : ),
166 0 : pages: stackedRouteVRoute.pages,
167 0 : pathParameters: stackedRouteVRoute.pathParameters,
168 0 : vRouteElements: <VRouteElement>[this] + stackedRouteVRoute.vRouteElements,
169 : );
170 : }
171 : }
172 :
173 : // Else, if no subroute is valid
174 :
175 : // check if this is an exact match with path
176 10 : final vRoute = getVRouteFromSelf(
177 : vPathRequestData,
178 : parentPathParameters: parentPathParameters,
179 0 : getPathMatchResult: pathGetPathMatchResult,
180 : );
181 : if (vRoute != null) {
182 : return vRoute;
183 : }
184 :
185 : // Check exact match for the aliases
186 28 : for (var i = 0; i < aliases.length; i++) {
187 1 : final vRoute = getVRouteFromSelf(
188 : vPathRequestData,
189 : parentPathParameters: parentPathParameters,
190 1 : getPathMatchResult: aliasesGetPathMatchResult[i],
191 : );
192 : if (vRoute != null) {
193 : return vRoute;
194 : }
195 : }
196 :
197 : // Else return null
198 : return null;
199 : }
200 :
201 : /// Searches for a valid [VRoute] by asking [VRouteElement]s is [routes] if they can form a valid [VRoute]
202 10 : VRoute? getVRouteFromRoutes(
203 : VPathRequestData vPathRequestData, {
204 : required List<VRouteElement> routes,
205 : required GetPathMatchResult getPathMatchResult,
206 : }) {
207 20 : for (var vRouteElement in routes) {
208 10 : final childVRoute = vRouteElement.buildRoute(
209 : vPathRequestData,
210 10 : parentRemainingPath: getPathMatchResult.remainingPath,
211 10 : parentPathParameters: getPathMatchResult.pathParameters,
212 : );
213 : if (childVRoute != null) return childVRoute;
214 : }
215 : }
216 :
217 : /// Try to form a [VRoute] where this [VRouteElement] is the last [VRouteElement]
218 : /// This is possible is:
219 : /// - [mustMatchSubRoute] is false
220 : /// - Their is a match of the path and it is exact
221 10 : VRoute? getVRouteFromSelf(
222 : VPathRequestData vPathRequestData, {
223 : required Map<String, String> parentPathParameters,
224 : required GetPathMatchResult getPathMatchResult,
225 : }) {
226 30 : if (!mustMatchSubRoute && (getPathMatchResult.remainingPath?.isEmpty ?? false)) {
227 10 : return VRoute(
228 10 : vRouteElementNode: VRouteElementNode(this),
229 10 : pages: [],
230 10 : pathParameters: getPathMatchResult.pathParameters,
231 10 : vRouteElements: <VRouteElement>[this],
232 : );
233 : }
234 : }
235 :
236 : /// Returns path information given a local path.
237 : ///
238 : /// [entirePath] is the whole path, useful when [selfPathRegExp] is absolute
239 : /// [remainingPathFromParent] is the path that remain after removing the parent paths, useful when [selfPathRegExp] relative
240 : /// [selfPathRegExp] the RegExp corresponding to the path that should be tested
241 : ///
242 : /// Returns a [GetPathMatchResult] which holds two information:
243 : /// - The remaining path, after having removed the [selfPathRegExp] (null if there is no match)
244 : /// - The path parameters gotten from [selfPathRegExp] and the path, added to the parentPathParameters if local path
245 10 : GetPathMatchResult getPathMatch({
246 : required String entirePath,
247 : required String? remainingPathFromParent,
248 : required String? selfPath,
249 : required RegExp? selfPathRegExp,
250 : required List<String> selfPathParametersKeys,
251 : required Map<String, String> parentPathParameters,
252 : }) {
253 : late final Match? match;
254 :
255 : // remainingPath is null if there is no match
256 : late String? remainingPath;
257 : late final Map<String, String> newPathParameters;
258 : if (selfPath == null) {
259 : // This is ugly but the only way to return a non-null empty match...
260 4 : match = RegExp('').matchAsPrefix('');
261 : remainingPath = remainingPathFromParent;
262 0 : newPathParameters = parentPathParameters;
263 10 : } else if ((selfPath.startsWith('/'))) {
264 : // If our path starts with '/', this is an absolute path
265 10 : match = selfPathRegExp!.matchAsPrefix(entirePath);
266 20 : remainingPath = (match != null) ? entirePath.substring(match.end) : null;
267 20 : newPathParameters = (match != null) ? extract(selfPathParametersKeys, match) : {};
268 : } else if ((remainingPathFromParent != null)) {
269 : // If it does not start with '/', the path is relative
270 : // We try to remove this part of the path from the remainingPathFromParent
271 2 : match = selfPathRegExp!.matchAsPrefix(remainingPathFromParent);
272 4 : remainingPath = (match != null) ? remainingPathFromParent.substring(match.end) : null;
273 0 : newPathParameters = (match != null)
274 2 : ? {
275 2 : ...parentPathParameters,
276 2 : ...extract(selfPathParametersKeys, match),
277 : }
278 2 : : {};
279 : } else {
280 : // If remainingPathFromParent is null and the path is relative
281 : // the parent did not match, so there is no match
282 0 : match = null;
283 : remainingPath = null;
284 1 : newPathParameters = {};
285 : }
286 :
287 : // Remove the trailing '/' in remainingPath if needed
288 10 : if (remainingPath != null && remainingPath.startsWith('/'))
289 2 : remainingPath = remainingPath.replaceFirst('/', '');
290 :
291 10 : return GetPathMatchResult(remainingPath: remainingPath, pathParameters: newPathParameters);
292 : }
293 :
294 : /// Tries to a path from a name
295 : ///
296 : /// This first asks its stackedRoutes if they have a match
297 : /// Else is tries to see if this [VRouteElement] matches the name
298 : /// Else return null
299 : ///
300 : /// Note that not only the name must match but the path parameters must be able to form a
301 : /// valid path afterward too
302 5 : String? getPathFromName(
303 : String nameToMatch, {
304 : required Map<String, String> pathParameters,
305 : required String? parentPath,
306 : required Map<String, String> remainingPathParameters,
307 : }) {
308 : // A variable to store the new parentPath from the path
309 : late final String? newParentPathFromPath;
310 : late final Map<String, String> newRemainingPathParametersFromPath;
311 :
312 : // A variable to store the new parent path from the aliases
313 5 : final List<String?> newParentPathFromAliases = [];
314 5 : final List<Map<String, String>> newRemainingPathParametersFromAliases = [];
315 :
316 : // Check if any subroute matches the name using path
317 :
318 : // Get the new parent path by taking this path into account
319 5 : newParentPathFromPath = getNewParentPath(parentPath,
320 10 : path: path, pathParametersKeys: pathParametersKeys, pathParameters: pathParameters);
321 :
322 15 : newRemainingPathParametersFromPath = (path != null && path!.startsWith('/'))
323 5 : ? Map<String, String>.from(pathParameters)
324 2 : : Map<String, String>.from(remainingPathParameters)
325 14 : ..removeWhere((key, value) => pathParametersKeys.contains(key));
326 :
327 10 : for (var vRouteElement in stackedRoutes) {
328 5 : String? childPathFromName = vRouteElement.getPathFromName(
329 : nameToMatch,
330 : pathParameters: pathParameters,
331 0 : parentPath: newParentPathFromPath,
332 0 : remainingPathParameters: newRemainingPathParametersFromPath,
333 : );
334 : if (childPathFromName != null) {
335 : return childPathFromName;
336 : }
337 : }
338 :
339 : // Check if any subroute matches the name using aliases
340 :
341 16 : for (var i = 0; i < aliases.length; i++) {
342 : // Get the new parent path by taking this alias into account
343 2 : newParentPathFromAliases.add(getNewParentPath(
344 : parentPath,
345 2 : path: aliases[i],
346 2 : pathParametersKeys: aliasesPathParametersKeys[i],
347 : pathParameters: pathParameters,
348 : ));
349 1 : newRemainingPathParametersFromAliases.add(
350 3 : (aliases[i].startsWith('/'))
351 1 : ? Map<String, String>.from(pathParameters)
352 0 : : Map<String, String>.from(remainingPathParameters)
353 5 : ..removeWhere((key, value) => aliasesPathParametersKeys[i].contains(key)),
354 : );
355 1 : for (var vRouteElement in stackedRoutes) {
356 0 : String? childPathFromName = vRouteElement.getPathFromName(
357 : nameToMatch,
358 : pathParameters: pathParameters,
359 0 : parentPath: newParentPathFromAliases[i],
360 0 : remainingPathParameters: newRemainingPathParametersFromAliases[i],
361 : );
362 : if (childPathFromName != null) {
363 : return childPathFromName;
364 : }
365 : }
366 : }
367 :
368 : // If no subroute matches the name, try to match this name
369 10 : if (name == nameToMatch) {
370 : // Note that newParentPath will be null if this path can't be included so the return value
371 : // is the right one
372 4 : if (newParentPathFromPath != null && newRemainingPathParametersFromPath.isEmpty) {
373 0 : return newParentPathFromPath;
374 : }
375 4 : for (var i = 0; i < aliases.length; i++) {
376 1 : if (newParentPathFromAliases[i] != null &&
377 2 : newRemainingPathParametersFromAliases[i].isEmpty) {
378 1 : return newParentPathFromAliases[i];
379 : }
380 : }
381 : }
382 :
383 : // Else we return null
384 : return null;
385 : }
386 :
387 : /// The goal is that, considering [path] and [parentPath] we can form a new parentPath
388 : ///
389 : /// If this path is null, then the new parentPath is the same as the old one
390 : /// If this path starts with '/':
391 : /// - Either the path parameters from [pathParameters] include those of this path and
392 : /// we return the corresponding path
393 : /// - Or we return null
394 : /// If this path does not start with '/':
395 : /// - If the parent path is null we return null
396 : /// - If the parent path is not null:
397 : /// * Either the path parameters from [pathParameters] include those of this path and
398 : /// we return the parent path + this path
399 : /// * Or we return null
400 : ///
401 : /// This method is used in [getPathFromPop]
402 9 : String? getNewParentPath(
403 : String? parentPath, {
404 : required String? path,
405 : required List<String> pathParametersKeys,
406 : required Map<String, String> pathParameters,
407 : }) {
408 : // First check that we have the path parameters needed to have this path
409 : final indexNoMatch =
410 19 : pathParametersKeys.indexWhere((key) => !pathParameters.containsKey(key));
411 :
412 : // If we have all the path parameters needed, get the local path
413 : final localPath =
414 36 : (indexNoMatch == -1 && path != null) ? pathToFunction(path)(pathParameters) : null;
415 :
416 : late final String? newParentPath;
417 : if (path == null) {
418 : // If the path is null, the new parent path is the same as the previous one
419 0 : newParentPath = parentPath;
420 9 : } else if (path.startsWith('/')) {
421 0 : newParentPath = localPath;
422 : } else if (parentPath == null) {
423 : // if we don't start with '/' and parent path is null, then newParentPath is also null
424 0 : newParentPath = null;
425 : } else {
426 : // If localPath is null, the pathParameters did not match so newParentPath is null
427 6 : newParentPath = (localPath != null) ? parentPath + '/' + localPath : null;
428 : }
429 :
430 0 : return newParentPath;
431 : }
432 :
433 8 : GetPathFromPopResult? getPathFromPop(
434 : VRouteElement elementToPop, {
435 : required Map<String, String> pathParameters,
436 : required String? parentPath,
437 : }) {
438 : // If vRouteElement is this, then this is the element to pop so we return null
439 8 : if (elementToPop == this) {
440 8 : return GetPathFromPopResult(path: parentPath, didPop: true);
441 : }
442 :
443 : // Try to match the path given the path parameters
444 8 : final newParentPathFromPath = getNewParentPath(
445 : parentPath,
446 8 : path: path,
447 8 : pathParametersKeys: pathParametersKeys,
448 : pathParameters: pathParameters,
449 : );
450 :
451 16 : print('newParentPathFromPath: $newParentPathFromPath');
452 :
453 : // If the path matched and produced a non null newParentPath, try to pop from the stackedRoutes
454 : if (newParentPathFromPath != null) {
455 16 : for (var vRouteElement in stackedRoutes) {
456 8 : final childPopResult = vRouteElement.getPathFromPop(
457 : elementToPop,
458 : pathParameters: pathParameters,
459 : parentPath: newParentPathFromPath,
460 : );
461 : if (childPopResult != null) {
462 24 : print('childPopResult.path: ${childPopResult.path}');
463 16 : return GetPathFromPopResult(path: childPopResult.path, didPop: false);
464 : }
465 : }
466 : }
467 :
468 : // Try to match the aliases given the path parameters
469 6 : for (var i = 0; i < aliases.length; i++) {
470 1 : final newParentPathFromAlias = getNewParentPath(
471 : parentPath,
472 2 : path: aliases[i],
473 2 : pathParametersKeys: aliasesPathParametersKeys[i],
474 : pathParameters: pathParameters,
475 : );
476 :
477 : // If an alias matched and produced a non null newParentPath, try to pop from the stackedRoutes
478 : if (newParentPathFromAlias != null) {
479 : // Try to pop from the stackedRoutes
480 2 : for (var vRouteElement in stackedRoutes) {
481 1 : final childPopResult = vRouteElement.getPathFromPop(
482 : elementToPop,
483 : pathParameters: pathParameters,
484 : parentPath: newParentPathFromAlias,
485 : );
486 : if (childPopResult != null) {
487 2 : return GetPathFromPopResult(path: childPopResult.path, didPop: false);
488 : }
489 : }
490 : }
491 : }
492 :
493 : // If none of the stackedRoutes popped and this did not pop, return a null result
494 : return null;
495 : }
496 : }
497 :
498 : /// Return type for [VRouteElementWithPath.getPathMatch]
499 : class GetPathMatchResult {
500 : final String? remainingPath;
501 : final Map<String, String> pathParameters;
502 :
503 10 : GetPathMatchResult({
504 : required this.remainingPath,
505 : required this.pathParameters,
506 : });
507 : }
|