1 | 'use strict';
|
2 |
|
3 | var defaultExport = (function (Error) {
|
4 | function defaultExport(route, path) {
|
5 | var message = "Unreachable '" + route + "', segment '" + path + "' is not defined";
|
6 | Error.call(this, message);
|
7 | this.message = message;
|
8 | }
|
9 |
|
10 | if ( Error ) defaultExport.__proto__ = Error;
|
11 | defaultExport.prototype = Object.create( Error && Error.prototype );
|
12 | defaultExport.prototype.constructor = defaultExport;
|
13 |
|
14 | return defaultExport;
|
15 | }(Error));
|
16 |
|
17 | function buildMatcher(path, parent) {
|
18 | var regex;
|
19 |
|
20 | var _isSplat;
|
21 |
|
22 | var _priority = -100;
|
23 |
|
24 | var keys = [];
|
25 | regex = path.replace(/[-$.]/g, '\\$&').replace(/\(/g, '(?:').replace(/\)/g, ')?').replace(/([:*]\w+)(?:<([^<>]+?)>)?/g, function (_, key, expr) {
|
26 | keys.push(key.substr(1));
|
27 |
|
28 | if (key.charAt() === ':') {
|
29 | _priority += 100;
|
30 | return ("((?!#)" + (expr || '[^#/]+?') + ")");
|
31 | }
|
32 |
|
33 | _isSplat = true;
|
34 | _priority += 500;
|
35 | return ("((?!#)" + (expr || '[^#]+?') + ")");
|
36 | });
|
37 |
|
38 | try {
|
39 | regex = new RegExp(("^" + regex + "$"));
|
40 | } catch (e) {
|
41 | throw new TypeError(("Invalid route expression, given '" + parent + "'"));
|
42 | }
|
43 |
|
44 | var _hashed = path.includes('#') ? 0.5 : 1;
|
45 |
|
46 | var _depth = path.length * _priority * _hashed;
|
47 |
|
48 | return {
|
49 | keys: keys,
|
50 | regex: regex,
|
51 | _depth: _depth,
|
52 | _isSplat: _isSplat
|
53 | };
|
54 | }
|
55 | var PathMatcher = function PathMatcher(path, parent) {
|
56 | var ref = buildMatcher(path, parent);
|
57 | var keys = ref.keys;
|
58 | var regex = ref.regex;
|
59 | var _depth = ref._depth;
|
60 | var _isSplat = ref._isSplat;
|
61 | return {
|
62 | _isSplat: _isSplat,
|
63 | _depth: _depth,
|
64 | match: function (value) {
|
65 | var matches = value.match(regex);
|
66 |
|
67 | if (matches) {
|
68 | return keys.reduce(function (prev, cur, i) {
|
69 | prev[cur] = typeof matches[i + 1] === 'string' ? decodeURIComponent(matches[i + 1]) : null;
|
70 | return prev;
|
71 | }, {});
|
72 | }
|
73 | }
|
74 | };
|
75 | };
|
76 |
|
77 | PathMatcher.push = function push (key, prev, leaf, parent) {
|
78 | var root = prev[key] || (prev[key] = {});
|
79 |
|
80 | if (!root.pattern) {
|
81 | root.pattern = new PathMatcher(key, parent);
|
82 | root.route = (leaf || '').replace(/\/$/, '') || '/';
|
83 | }
|
84 |
|
85 | prev.keys = prev.keys || [];
|
86 |
|
87 | if (!prev.keys.includes(key)) {
|
88 | prev.keys.push(key);
|
89 | PathMatcher.sort(prev);
|
90 | }
|
91 |
|
92 | return root;
|
93 | };
|
94 |
|
95 | PathMatcher.sort = function sort (root) {
|
96 | root.keys.sort(function (a, b) {
|
97 | return root[a].pattern._depth - root[b].pattern._depth;
|
98 | });
|
99 | };
|
100 |
|
101 | function merge(path, parent) {
|
102 | return ("" + (parent && parent !== '/' ? parent : '') + (path || ''));
|
103 | }
|
104 | function walk(path, cb) {
|
105 | var matches = path.match(/<[^<>]*\/[^<>]*>/);
|
106 |
|
107 | if (matches) {
|
108 | throw new TypeError(("RegExp cannot contain slashes, given '" + matches + "'"));
|
109 | }
|
110 |
|
111 | var parts = path !== '/' ? path.split('/') : [''];
|
112 | var root = [];
|
113 | parts.some(function (x, i) {
|
114 | var parent = root.concat(x).join('/') || null;
|
115 | var segment = parts.slice(i + 1).join('/') || null;
|
116 | var retval = cb(("/" + x), parent, segment ? ((x ? ("/" + x) : '') + "/" + segment) : null);
|
117 | root.push(x);
|
118 | return retval;
|
119 | });
|
120 | }
|
121 | function reduce(key, root, _seen) {
|
122 | var params = {};
|
123 | var out = [];
|
124 | var splat;
|
125 | walk(key, function (x, leaf, extra) {
|
126 | var found;
|
127 |
|
128 | if (!root.keys) {
|
129 | throw new defaultExport(key, x);
|
130 | }
|
131 |
|
132 | root.keys.some(function (k) {
|
133 | if (_seen.includes(k)) { return false; }
|
134 | var ref = root[k].pattern;
|
135 | var match = ref.match;
|
136 | var _isSplat = ref._isSplat;
|
137 | var matches = match(_isSplat ? extra || x : x);
|
138 |
|
139 | if (matches) {
|
140 | Object.assign(params, matches);
|
141 |
|
142 | if (root[k].route) {
|
143 | var routeInfo = Object.assign({}, root[k].info);
|
144 |
|
145 | var hasMatch = false;
|
146 |
|
147 | if (routeInfo.exact) {
|
148 | hasMatch = extra === null;
|
149 | } else {
|
150 | hasMatch = x && leaf === null || x === leaf || _isSplat || !extra;
|
151 | }
|
152 |
|
153 | routeInfo.matches = hasMatch;
|
154 | routeInfo.params = Object.assign({}, params);
|
155 | routeInfo.route = root[k].route;
|
156 | routeInfo.path = _isSplat ? extra : leaf || x;
|
157 | out.push(routeInfo);
|
158 | }
|
159 |
|
160 | if (extra === null && !root[k].keys) {
|
161 | return true;
|
162 | }
|
163 |
|
164 | if (k !== '/') { _seen.push(k); }
|
165 | splat = _isSplat;
|
166 | root = root[k];
|
167 | found = true;
|
168 | return true;
|
169 | }
|
170 |
|
171 | return false;
|
172 | });
|
173 |
|
174 | if (!(found || root.keys.some(function (k) { return root[k].pattern.match(x); }))) {
|
175 | throw new defaultExport(key, x);
|
176 | }
|
177 |
|
178 | return splat || !found;
|
179 | });
|
180 | return out;
|
181 | }
|
182 | function find(path, routes, retries) {
|
183 | var get = reduce.bind(null, path, routes);
|
184 | var set = [];
|
185 |
|
186 | while (retries > 0) {
|
187 | retries -= 1;
|
188 |
|
189 | try {
|
190 | return get(set);
|
191 | } catch (e) {
|
192 | if (retries > 0) {
|
193 | return get(set);
|
194 | }
|
195 |
|
196 | throw e;
|
197 | }
|
198 | }
|
199 | }
|
200 | function add(path, routes, parent, routeInfo) {
|
201 | var fullpath = merge(path, parent);
|
202 | var root = routes;
|
203 | var key;
|
204 |
|
205 | if (routeInfo && routeInfo.nested !== true) {
|
206 | key = routeInfo.key;
|
207 | delete routeInfo.key;
|
208 | }
|
209 |
|
210 | walk(fullpath, function (x, leaf) {
|
211 | root = PathMatcher.push(x, root, leaf, fullpath);
|
212 |
|
213 | if (x !== '/') {
|
214 | root.info = root.info || Object.assign({}, routeInfo);
|
215 | }
|
216 | });
|
217 | root.info = root.info || Object.assign({}, routeInfo);
|
218 |
|
219 | if (key) {
|
220 | root.info.key = key;
|
221 | }
|
222 |
|
223 | return fullpath;
|
224 | }
|
225 | function rm(path, routes, parent) {
|
226 | var fullpath = merge(path, parent);
|
227 | var root = routes;
|
228 | var leaf = null;
|
229 | var key = null;
|
230 | walk(fullpath, function (x) {
|
231 | if (!root) {
|
232 | leaf = null;
|
233 | return true;
|
234 | }
|
235 |
|
236 | key = x;
|
237 | leaf = x === '/' ? routes['/'] : root;
|
238 |
|
239 | if (!leaf.keys) {
|
240 | throw new defaultExport(path, x);
|
241 | }
|
242 |
|
243 | root = root[x];
|
244 | });
|
245 |
|
246 | if (!(leaf && key)) {
|
247 | throw new defaultExport(path, key);
|
248 | }
|
249 |
|
250 | delete leaf[key];
|
251 |
|
252 | if (key === '/') {
|
253 | delete leaf.info;
|
254 | delete leaf.route;
|
255 | }
|
256 |
|
257 | var offset = leaf.keys.indexOf(key);
|
258 |
|
259 | if (offset !== -1) {
|
260 | leaf.keys.splice(leaf.keys.indexOf(key), 1);
|
261 | PathMatcher.sort(leaf);
|
262 | }
|
263 | }
|
264 |
|
265 | var Router = function Router() {
|
266 | var routes = {};
|
267 | var stack = [];
|
268 | return {
|
269 | resolve: function (path, cb) {
|
270 | var ref = path.split(/(?=[#?])/);
|
271 | var uri = ref[0];
|
272 | var hash = ref[1];
|
273 | var query = ref[2];
|
274 | var segments = uri.substr(1).split('/');
|
275 | var prefix = [];
|
276 | var seen = [];
|
277 | segments.some(function (key) {
|
278 | var sub = prefix.concat(("/" + key)).join('');
|
279 | if (key.length) { prefix.push(("/" + key)); }
|
280 |
|
281 | try {
|
282 | var next = find(sub, routes, 1);
|
283 | cb(null, next.filter(function (x) {
|
284 | if (!seen.includes(x.route)) {
|
285 | seen.push(x.route);
|
286 | return true;
|
287 | }
|
288 |
|
289 | return false;
|
290 | }));
|
291 | } catch (e) {
|
292 | cb(e, []);
|
293 | return true;
|
294 | }
|
295 |
|
296 | return false;
|
297 | });
|
298 |
|
299 | if (hash) {
|
300 | cb(null, find(("" + uri + hash), routes, 1));
|
301 | }
|
302 | },
|
303 | mount: function (path, cb) {
|
304 | if (path !== '/') {
|
305 | stack.push(path);
|
306 | }
|
307 |
|
308 | cb();
|
309 | stack.pop();
|
310 | },
|
311 | find: function (path, retries) { return find(path, routes, retries === true ? 2 : retries || 1); },
|
312 | add: function (path, routeInfo) { return add(path, routes, stack.join(''), routeInfo); },
|
313 | rm: function (path) { return rm(path, routes, stack.join('')); }
|
314 | };
|
315 | };
|
316 |
|
317 | module.exports = Router;
|