tree.dart 6.5 KB

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