Line data Source code
1 : import 'package:path/path.dart' as path; 2 : import 'package:routemaster/routemaster.dart'; 3 : import '../../query_parser.dart'; 4 : import 'errors.dart'; 5 : import 'router_result.dart'; 6 : import 'trie_node.dart'; 7 : 8 : class TrieRouter { 9 : final Trie<String, PageBuilder> _trie; 10 : 11 20 : TrieRouter() : _trie = Trie(); 12 : 13 9 : void addAll(Map<String, PageBuilder> routes) { 14 18 : routes.forEach((key, value) { 15 9 : add(key, value); 16 : }); 17 : } 18 : 19 : /// Throws a [ConflictingPathError] if there is a conflict. 20 : /// 21 : /// It is an error to add two segments prefixed with ':' at the same index. 22 10 : bool add(String route, PageBuilder value) { 23 10 : assert(route.isNotEmpty); 24 : 25 10 : var pathSegments = path.split(route); 26 10 : return addPathComponents(pathSegments, value); 27 : } 28 : 29 : /// Throws a [ConflictingPathError] if there is a conflict. 30 : /// 31 : /// It is an error to add two segments prefixed with ':' at the same index. 32 10 : bool addPathComponents(Iterable<String> pathSegments, PageBuilder value) { 33 10 : assert(pathSegments.isNotEmpty); 34 : 35 10 : var list = List<String>.from(pathSegments); 36 20 : TrieNode<String?, PageBuilder?>? current = _trie.root; 37 : var isNew = false; 38 : 39 : // Work downwards through the trie, adding nodes as needed, and keeping 40 : // track of whether we add any nodes. 41 30 : for (var i = 0; i < list.length; i++) { 42 10 : var pathSegment = list[i]; 43 : 44 : // Throw an error when two segments start with ':' at the same index. 45 10 : if (pathSegment.startsWith(':') && 46 8 : current!.containsWhere((k) => k!.startsWith(':')) && 47 6 : !current.containsWhere((k) => k == pathSegment)) { 48 1 : throw ConflictingPathError( 49 : list, 50 2 : List<String?>.from(list).sublist(0, i) 51 5 : ..add(current.getWhere((k) => k!.startsWith(':'))!.key)); 52 : } 53 : 54 10 : if (!current!.contains(pathSegment)) { 55 : isNew = true; 56 : 57 30 : final isLastSegment = i == list.length - 1; 58 : if (isLastSegment) { 59 10 : current.add(pathSegment, value); 60 : } else { 61 3 : current.add(pathSegment, null); 62 : } 63 : } 64 : 65 10 : current = current.get(pathSegment); 66 : } 67 : 68 : // Explicitly mark the end of a list of path segments. Otherwise, we might 69 : // say a path segment is present if it is a prefix of a different, longer 70 : // word that was added earlier. 71 10 : if (!current!.contains(null)) { 72 : isNew = true; 73 10 : current.add(null, null); 74 : } 75 : return isNew; 76 : } 77 : 78 7 : RouterResult? get(String route) { 79 14 : var pathSegments = path.split(QueryParser.stripQueryString(route)); 80 7 : var parameters = <String, String>{}; 81 14 : TrieNode<String?, PageBuilder?>? current = _trie.root; 82 : 83 14 : for (var segment in pathSegments) { 84 7 : if (current!.contains(segment)) { 85 7 : current = current.get(segment); 86 4 : } else if (current.containsWhere((k) => k!.startsWith(':'))) { 87 : // If there is a segment that starts with `:`, we should match any 88 : // route. 89 3 : current = current.getWhere((k) => k != null && k.startsWith(':')); 90 : 91 : // Add the current segment to the parameters. E.g. 'id': '123' 92 3 : parameters[current!.key!.substring(1)] = segment; 93 : } else { 94 : return null; 95 : } 96 : } 97 : 98 14 : return RouterResult(current!.value!, parameters, route); 99 : } 100 : 101 10 : List<RouterResult>? getAll(String route) { 102 20 : var pathSegments = path.split(QueryParser.stripQueryString(route)); 103 10 : var parameters = <String, String>{}; 104 10 : final result = <RouterResult>[]; 105 : 106 10 : void addToResult(int index, TrieNode<String?, PageBuilder?> node) { 107 20 : final p = path.joinAll(pathSegments.take(index)); 108 10 : result.add( 109 10 : RouterResult( 110 10 : node.value!, 111 10 : Map.unmodifiable(parameters), 112 : p, 113 : ), 114 : ); 115 : } 116 : 117 20 : TrieNode<String?, PageBuilder?>? current = _trie.root; 118 : 119 : var i = 0; 120 : 121 20 : for (var segment in pathSegments) { 122 10 : i++; 123 : 124 10 : if (current!.contains(segment)) { 125 10 : current = current.get(segment); 126 10 : if (current!.value != null) { 127 10 : addToResult(i, current); 128 : } 129 15 : } else if (current.containsWhere((k) => k!.startsWith(':'))) { 130 : final nextSegment = 131 14 : i < pathSegments.length - 1 ? pathSegments[i] : null; 132 2 : final nextSegmentIsParam = nextSegment?.startsWith(':') ?? false; 133 : 134 : // If there is a segment that starts with `:`, we should match any 135 : // route. 136 12 : current = current.getWhere((k) => k != null && k.startsWith(':')); 137 : 138 : // Add the current segment to the parameters. E.g. ':id': '123' 139 12 : parameters[current!.key!.substring(1)] = segment; 140 : 141 4 : if (!nextSegmentIsParam && current.value != null) { 142 3 : addToResult(i, current); 143 : } 144 : } else { 145 : return null; 146 : } 147 : } 148 : 149 : return result; 150 : } 151 : }