tree.dart 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. /*
  2. * fluro
  3. * Created by Yakka
  4. * https://theyakka.com
  5. *
  6. * Copyright (c) 2019 Yakka, LLC. All rights reserved.
  7. * See LICENSE for distribution and usage details.
  8. */
  9. import 'package:fluro/src/common.dart';
  10. import 'package:flutter/widgets.dart';
  11. enum RouteTreeNodeType {
  12. component,
  13. parameter,
  14. }
  15. class AppRouteMatch {
  16. // constructors
  17. AppRouteMatch(this.route);
  18. // properties
  19. AppRoute route;
  20. Map<String, List<String>> parameters = <String, List<String>>{};
  21. }
  22. class RouteTreeNodeMatch {
  23. // constructors
  24. RouteTreeNodeMatch(this.node);
  25. RouteTreeNodeMatch.fromMatch(RouteTreeNodeMatch match, this.node) {
  26. parameters = <String, List<String>>{};
  27. if (match != null) {
  28. parameters.addAll(match.parameters);
  29. }
  30. }
  31. // properties
  32. RouteTreeNode node;
  33. Map<String, List<String>> parameters = <String, List<String>>{};
  34. }
  35. class RouteTreeNode {
  36. // constructors
  37. RouteTreeNode(this.part, this.type);
  38. // properties
  39. String part;
  40. RouteTreeNodeType type;
  41. List<AppRoute> routes = <AppRoute>[];
  42. List<RouteTreeNode> nodes = <RouteTreeNode>[];
  43. RouteTreeNode parent;
  44. bool isParameter() {
  45. return type == RouteTreeNodeType.parameter;
  46. }
  47. }
  48. class RouteTree {
  49. // private
  50. final List<RouteTreeNode> _nodes = <RouteTreeNode>[];
  51. bool _hasDefaultRoute = false;
  52. // addRoute - add a route to the route tree
  53. void addRoute(AppRoute route) {
  54. String path = route.route;
  55. // is root/default route, just add it
  56. if (path == Navigator.defaultRouteName) {
  57. if (_hasDefaultRoute) {
  58. // throw an error because the internal consistency of the router
  59. // could be affected
  60. throw ("Default route was already defined");
  61. }
  62. var node = RouteTreeNode(path, RouteTreeNodeType.component);
  63. node.routes = [route];
  64. _nodes.add(node);
  65. _hasDefaultRoute = true;
  66. return;
  67. }
  68. if (path.startsWith("/")) {
  69. path = path.substring(1);
  70. }
  71. List<String> pathComponents = path.split('/');
  72. RouteTreeNode parent;
  73. for (int i = 0; i < pathComponents.length; i++) {
  74. String component = pathComponents[i];
  75. RouteTreeNode node = _nodeForComponent(component, parent);
  76. if (node == null) {
  77. RouteTreeNodeType type = _typeForComponent(component);
  78. node = RouteTreeNode(component, type);
  79. node.parent = parent;
  80. if (parent == null) {
  81. _nodes.add(node);
  82. } else {
  83. parent.nodes.add(node);
  84. }
  85. }
  86. if (i == pathComponents.length - 1) {
  87. if (node.routes == null) {
  88. node.routes = [route];
  89. } else {
  90. node.routes.add(route);
  91. }
  92. }
  93. parent = node;
  94. }
  95. }
  96. AppRouteMatch matchRoute(String path) {
  97. String usePath = path;
  98. if (usePath.startsWith("/")) {
  99. usePath = path.substring(1);
  100. }
  101. List<String> components = usePath.split("/");
  102. if (path == Navigator.defaultRouteName) {
  103. components = ["/"];
  104. }
  105. Map<RouteTreeNode, RouteTreeNodeMatch> nodeMatches =
  106. <RouteTreeNode, RouteTreeNodeMatch>{};
  107. List<RouteTreeNode> nodesToCheck = _nodes;
  108. for (String checkComponent in components) {
  109. Map<RouteTreeNode, RouteTreeNodeMatch> currentMatches =
  110. <RouteTreeNode, RouteTreeNodeMatch>{};
  111. List<RouteTreeNode> nextNodes = <RouteTreeNode>[];
  112. for (RouteTreeNode node in nodesToCheck) {
  113. String pathPart = checkComponent;
  114. Map<String, List<String>> queryMap;
  115. if (checkComponent.contains("?")) {
  116. var splitParam = checkComponent.split("?");
  117. pathPart = splitParam[0];
  118. queryMap = parseQueryString(splitParam[1]);
  119. }
  120. bool isMatch = (node.part == pathPart || node.isParameter());
  121. if (isMatch) {
  122. RouteTreeNodeMatch parentMatch = nodeMatches[node.parent];
  123. RouteTreeNodeMatch match =
  124. RouteTreeNodeMatch.fromMatch(parentMatch, node);
  125. if (node.isParameter()) {
  126. String paramKey = node.part.substring(1);
  127. match.parameters[paramKey] = [pathPart];
  128. }
  129. if (queryMap != null) {
  130. match.parameters.addAll(queryMap);
  131. }
  132. // print("matched: ${node.part}, isParam: ${node.isParameter()}, params: ${match.parameters}");
  133. currentMatches[node] = match;
  134. if (node.nodes != null) {
  135. nextNodes.addAll(node.nodes);
  136. }
  137. }
  138. }
  139. nodeMatches = currentMatches;
  140. nodesToCheck = nextNodes;
  141. if (currentMatches.values.length == 0) {
  142. return null;
  143. }
  144. }
  145. List<RouteTreeNodeMatch> matches = nodeMatches.values.toList();
  146. if (matches.length > 0) {
  147. RouteTreeNodeMatch match = matches.first;
  148. RouteTreeNode nodeToUse = match.node;
  149. // print("using match: ${match}, ${nodeToUse?.part}, ${match?.parameters}");
  150. if (nodeToUse != null &&
  151. nodeToUse.routes != null &&
  152. nodeToUse.routes.length > 0) {
  153. List<AppRoute> routes = nodeToUse.routes;
  154. AppRouteMatch routeMatch = AppRouteMatch(routes[0]);
  155. routeMatch.parameters = match.parameters;
  156. return routeMatch;
  157. }
  158. }
  159. return null;
  160. }
  161. void printTree() {
  162. _printSubTree();
  163. }
  164. void _printSubTree({RouteTreeNode parent, int level = 0}) {
  165. List<RouteTreeNode> nodes = parent != null ? parent.nodes : _nodes;
  166. for (RouteTreeNode node in nodes) {
  167. String indent = "";
  168. for (int i = 0; i < level; i++) {
  169. indent += " ";
  170. }
  171. print("$indent${node.part}: total routes=${node.routes.length}");
  172. if (node.nodes != null && node.nodes.length > 0) {
  173. _printSubTree(parent: node, level: level + 1);
  174. }
  175. }
  176. }
  177. RouteTreeNode _nodeForComponent(String component, RouteTreeNode parent) {
  178. List<RouteTreeNode> nodes = _nodes;
  179. if (parent != null) {
  180. // search parent for sub-node matches
  181. nodes = parent.nodes;
  182. }
  183. for (RouteTreeNode node in nodes) {
  184. if (node.part == component) {
  185. return node;
  186. }
  187. }
  188. return null;
  189. }
  190. RouteTreeNodeType _typeForComponent(String component) {
  191. RouteTreeNodeType type = RouteTreeNodeType.component;
  192. if (_isParameterComponent(component)) {
  193. type = RouteTreeNodeType.parameter;
  194. }
  195. return type;
  196. }
  197. /// Is the path component a parameter
  198. bool _isParameterComponent(String component) {
  199. return component.startsWith(":");
  200. }
  201. Map<String, List<String>> parseQueryString(String query) {
  202. var search = RegExp('([^&=]+)=?([^&]*)');
  203. var params = Map<String, List<String>>();
  204. if (query.startsWith('?')) query = query.substring(1);
  205. decode(String s) => Uri.decodeComponent(s.replaceAll('+', ' '));
  206. for (Match match in search.allMatches(query)) {
  207. String key = decode(match.group(1));
  208. String value = decode(match.group(2));
  209. if (params.containsKey(key)) {
  210. params[key].add(value);
  211. } else {
  212. params[key] = [value];
  213. }
  214. }
  215. return params;
  216. }
  217. }