UNPKG

22.8 kBJavaScriptView Raw
1var createObject = Object.create;
2function createMap() {
3 var map = createObject(null);
4 map["__"] = undefined;
5 delete map["__"];
6 return map;
7}
8
9var Target = function Target(path, matcher, delegate) {
10 this.path = path;
11 this.matcher = matcher;
12 this.delegate = delegate;
13};
14Target.prototype.to = function to (target, callback) {
15 var delegate = this.delegate;
16 if (delegate && delegate.willAddRoute) {
17 target = delegate.willAddRoute(this.matcher.target, target);
18 }
19 this.matcher.add(this.path, target);
20 if (callback) {
21 if (callback.length === 0) {
22 throw new Error("You must have an argument in the function passed to `to`");
23 }
24 this.matcher.addChild(this.path, target, callback, this.delegate);
25 }
26};
27var Matcher = function Matcher(target) {
28 this.routes = createMap();
29 this.children = createMap();
30 this.target = target;
31};
32Matcher.prototype.add = function add (path, target) {
33 this.routes[path] = target;
34};
35Matcher.prototype.addChild = function addChild (path, target, callback, delegate) {
36 var matcher = new Matcher(target);
37 this.children[path] = matcher;
38 var match = generateMatch(path, matcher, delegate);
39 if (delegate && delegate.contextEntered) {
40 delegate.contextEntered(target, match);
41 }
42 callback(match);
43};
44function generateMatch(startingPath, matcher, delegate) {
45 function match(path, callback) {
46 var fullPath = startingPath + path;
47 if (callback) {
48 callback(generateMatch(fullPath, matcher, delegate));
49 }
50 else {
51 return new Target(fullPath, matcher, delegate);
52 }
53 }
54
55 return match;
56}
57function addRoute(routeArray, path, handler) {
58 var len = 0;
59 for (var i = 0; i < routeArray.length; i++) {
60 len += routeArray[i].path.length;
61 }
62 path = path.substr(len);
63 var route = { path: path, handler: handler };
64 routeArray.push(route);
65}
66function eachRoute(baseRoute, matcher, callback, binding) {
67 var routes = matcher.routes;
68 var paths = Object.keys(routes);
69 for (var i = 0; i < paths.length; i++) {
70 var path = paths[i];
71 var routeArray = baseRoute.slice();
72 addRoute(routeArray, path, routes[path]);
73 var nested = matcher.children[path];
74 if (nested) {
75 eachRoute(routeArray, nested, callback, binding);
76 }
77 else {
78 callback.call(binding, routeArray);
79 }
80 }
81}
82var map = function (callback, addRouteCallback) {
83 var matcher = new Matcher();
84 callback(generateMatch("", matcher, this.delegate));
85 eachRoute([], matcher, function (routes) {
86 if (addRouteCallback) {
87 addRouteCallback(this, routes);
88 }
89 else {
90 this.add(routes);
91 }
92 }, this);
93};
94
95// Normalizes percent-encoded values in `path` to upper-case and decodes percent-encoded
96// values that are not reserved (i.e., unicode characters, emoji, etc). The reserved
97// chars are "/" and "%".
98// Safe to call multiple times on the same path.
99// Normalizes percent-encoded values in `path` to upper-case and decodes percent-encoded
100function normalizePath(path) {
101 return path.split("/")
102 .map(normalizeSegment)
103 .join("/");
104}
105// We want to ensure the characters "%" and "/" remain in percent-encoded
106// form when normalizing paths, so replace them with their encoded form after
107// decoding the rest of the path
108var SEGMENT_RESERVED_CHARS = /%|\//g;
109function normalizeSegment(segment) {
110 if (segment.length < 3 || segment.indexOf("%") === -1)
111 { return segment; }
112 return decodeURIComponent(segment).replace(SEGMENT_RESERVED_CHARS, encodeURIComponent);
113}
114// We do not want to encode these characters when generating dynamic path segments
115// See https://tools.ietf.org/html/rfc3986#section-3.3
116// sub-delims: "!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="
117// others allowed by RFC 3986: ":", "@"
118//
119// First encode the entire path segment, then decode any of the encoded special chars.
120//
121// The chars "!", "'", "(", ")", "*" do not get changed by `encodeURIComponent`,
122// so the possible encoded chars are:
123// ['%24', '%26', '%2B', '%2C', '%3B', '%3D', '%3A', '%40'].
124var PATH_SEGMENT_ENCODINGS = /%(?:2(?:4|6|B|C)|3(?:B|D|A)|40)/g;
125function encodePathSegment(str) {
126 return encodeURIComponent(str).replace(PATH_SEGMENT_ENCODINGS, decodeURIComponent);
127}
128
129var escapeRegex = /(\/|\.|\*|\+|\?|\||\(|\)|\[|\]|\{|\}|\\)/g;
130var isArray = Array.isArray;
131var hasOwnProperty = Object.prototype.hasOwnProperty;
132function getParam(params, key) {
133 if (typeof params !== "object" || params === null) {
134 throw new Error("You must pass an object as the second argument to `generate`.");
135 }
136 if (!hasOwnProperty.call(params, key)) {
137 throw new Error("You must provide param `" + key + "` to `generate`.");
138 }
139 var value = params[key];
140 var str = typeof value === "string" ? value : "" + value;
141 if (str.length === 0) {
142 throw new Error("You must provide a param `" + key + "`.");
143 }
144 return str;
145}
146var eachChar = [];
147eachChar[0 /* Static */] = function (segment, currentState) {
148 var state = currentState;
149 var value = segment.value;
150 for (var i = 0; i < value.length; i++) {
151 var ch = value.charCodeAt(i);
152 state = state.put(ch, false, false);
153 }
154 return state;
155};
156eachChar[1 /* Dynamic */] = function (_, currentState) {
157 return currentState.put(47 /* SLASH */, true, true);
158};
159eachChar[2 /* Star */] = function (_, currentState) {
160 return currentState.put(-1 /* ANY */, false, true);
161};
162eachChar[4 /* Epsilon */] = function (_, currentState) {
163 return currentState;
164};
165var regex = [];
166regex[0 /* Static */] = function (segment) {
167 return segment.value.replace(escapeRegex, "\\$1");
168};
169regex[1 /* Dynamic */] = function () {
170 return "([^/]+)";
171};
172regex[2 /* Star */] = function () {
173 return "(.+)";
174};
175regex[4 /* Epsilon */] = function () {
176 return "";
177};
178var generate = [];
179generate[0 /* Static */] = function (segment) {
180 return segment.value;
181};
182generate[1 /* Dynamic */] = function (segment, params) {
183 var value = getParam(params, segment.value);
184 if (RouteRecognizer.ENCODE_AND_DECODE_PATH_SEGMENTS) {
185 return encodePathSegment(value);
186 }
187 else {
188 return value;
189 }
190};
191generate[2 /* Star */] = function (segment, params) {
192 return getParam(params, segment.value);
193};
194generate[4 /* Epsilon */] = function () {
195 return "";
196};
197var EmptyObject = Object.freeze({});
198var EmptyArray = Object.freeze([]);
199// The `names` will be populated with the paramter name for each dynamic/star
200// segment. `shouldDecodes` will be populated with a boolean for each dyanamic/star
201// segment, indicating whether it should be decoded during recognition.
202function parse(segments, route, types) {
203 // normalize route as not starting with a "/". Recognition will
204 // also normalize.
205 if (route.length > 0 && route.charCodeAt(0) === 47 /* SLASH */) {
206 route = route.substr(1);
207 }
208 var parts = route.split("/");
209 var names = undefined;
210 var shouldDecodes = undefined;
211 for (var i = 0; i < parts.length; i++) {
212 var part = parts[i];
213 var flags = 0;
214 var type = 0;
215 if (part === "") {
216 type = 4 /* Epsilon */;
217 }
218 else if (part.charCodeAt(0) === 58 /* COLON */) {
219 type = 1 /* Dynamic */;
220 }
221 else if (part.charCodeAt(0) === 42 /* STAR */) {
222 type = 2 /* Star */;
223 }
224 else {
225 type = 0 /* Static */;
226 }
227 flags = 2 << type;
228 if (flags & 12 /* Named */) {
229 part = part.slice(1);
230 names = names || [];
231 names.push(part);
232 shouldDecodes = shouldDecodes || [];
233 shouldDecodes.push((flags & 4 /* Decoded */) !== 0);
234 }
235 if (flags & 14 /* Counted */) {
236 types[type]++;
237 }
238 segments.push({
239 type: type,
240 value: normalizeSegment(part)
241 });
242 }
243 return {
244 names: names || EmptyArray,
245 shouldDecodes: shouldDecodes || EmptyArray,
246 };
247}
248function isEqualCharSpec(spec, char, negate) {
249 return spec.char === char && spec.negate === negate;
250}
251// A State has a character specification and (`charSpec`) and a list of possible
252// subsequent states (`nextStates`).
253//
254// If a State is an accepting state, it will also have several additional
255// properties:
256//
257// * `regex`: A regular expression that is used to extract parameters from paths
258// that reached this accepting state.
259// * `handlers`: Information on how to convert the list of captures into calls
260// to registered handlers with the specified parameters
261// * `types`: How many static, dynamic or star segments in this route. Used to
262// decide which route to use if multiple registered routes match a path.
263//
264// Currently, State is implemented naively by looping over `nextStates` and
265// comparing a character specification against a character. A more efficient
266// implementation would use a hash of keys pointing at one or more next states.
267var State = function State(states, id, char, negate, repeat) {
268 this.states = states;
269 this.id = id;
270 this.char = char;
271 this.negate = negate;
272 this.nextStates = repeat ? id : null;
273 this.pattern = "";
274 this._regex = undefined;
275 this.handlers = undefined;
276 this.types = undefined;
277};
278State.prototype.regex = function regex$1 () {
279 if (!this._regex) {
280 this._regex = new RegExp(this.pattern);
281 }
282 return this._regex;
283};
284State.prototype.get = function get (char, negate) {
285 var this$1 = this;
286
287 var nextStates = this.nextStates;
288 if (nextStates === null)
289 { return; }
290 if (isArray(nextStates)) {
291 for (var i = 0; i < nextStates.length; i++) {
292 var child = this$1.states[nextStates[i]];
293 if (isEqualCharSpec(child, char, negate)) {
294 return child;
295 }
296 }
297 }
298 else {
299 var child$1 = this.states[nextStates];
300 if (isEqualCharSpec(child$1, char, negate)) {
301 return child$1;
302 }
303 }
304};
305State.prototype.put = function put (char, negate, repeat) {
306 var state;
307 // If the character specification already exists in a child of the current
308 // state, just return that state.
309 if (state = this.get(char, negate)) {
310 return state;
311 }
312 // Make a new state for the character spec
313 var states = this.states;
314 state = new State(states, states.length, char, negate, repeat);
315 states[states.length] = state;
316 // Insert the new state as a child of the current state
317 if (this.nextStates == null) {
318 this.nextStates = state.id;
319 }
320 else if (isArray(this.nextStates)) {
321 this.nextStates.push(state.id);
322 }
323 else {
324 this.nextStates = [this.nextStates, state.id];
325 }
326 // Return the new state
327 return state;
328};
329// Find a list of child states matching the next character
330State.prototype.match = function match (ch) {
331 var this$1 = this;
332
333 var nextStates = this.nextStates;
334 if (!nextStates)
335 { return []; }
336 var returned = [];
337 if (isArray(nextStates)) {
338 for (var i = 0; i < nextStates.length; i++) {
339 var child = this$1.states[nextStates[i]];
340 if (isMatch(child, ch)) {
341 returned.push(child);
342 }
343 }
344 }
345 else {
346 var child$1 = this.states[nextStates];
347 if (isMatch(child$1, ch)) {
348 returned.push(child$1);
349 }
350 }
351 return returned;
352};
353function isMatch(spec, char) {
354 return spec.negate ? spec.char !== char && spec.char !== -1 /* ANY */ : spec.char === char || spec.char === -1 /* ANY */;
355}
356// This is a somewhat naive strategy, but should work in a lot of cases
357// A better strategy would properly resolve /posts/:id/new and /posts/edit/:id.
358//
359// This strategy generally prefers more static and less dynamic matching.
360// Specifically, it
361//
362// * prefers fewer stars to more, then
363// * prefers using stars for less of the match to more, then
364// * prefers fewer dynamic segments to more, then
365// * prefers more static segments to more
366function sortSolutions(states) {
367 return states.sort(function (a, b) {
368 var ref = a.types || [0, 0, 0];
369 var astatics = ref[0];
370 var adynamics = ref[1];
371 var astars = ref[2];
372 var ref$1 = b.types || [0, 0, 0];
373 var bstatics = ref$1[0];
374 var bdynamics = ref$1[1];
375 var bstars = ref$1[2];
376 if (astars !== bstars) {
377 return astars - bstars;
378 }
379 if (astars) {
380 if (astatics !== bstatics) {
381 return bstatics - astatics;
382 }
383 if (adynamics !== bdynamics) {
384 return bdynamics - adynamics;
385 }
386 }
387 if (adynamics !== bdynamics) {
388 return adynamics - bdynamics;
389 }
390 if (astatics !== bstatics) {
391 return bstatics - astatics;
392 }
393 return 0;
394 });
395}
396function recognizeChar(states, ch) {
397 var nextStates = [];
398 for (var i = 0, l = states.length; i < l; i++) {
399 var state = states[i];
400 nextStates = nextStates.concat(state.match(ch));
401 }
402 return nextStates;
403}
404var RecognizeResults = function RecognizeResults(queryParams) {
405 this.length = 0;
406 this.queryParams = queryParams || {};
407};
408
409RecognizeResults.prototype.splice = Array.prototype.splice;
410RecognizeResults.prototype.slice = Array.prototype.slice;
411RecognizeResults.prototype.push = Array.prototype.push;
412function findHandler(state, originalPath, queryParams) {
413 var handlers = state.handlers;
414 var regex = state.regex();
415 if (!regex || !handlers)
416 { throw new Error("state not initialized"); }
417 var captures = originalPath.match(regex);
418 var currentCapture = 1;
419 var result = new RecognizeResults(queryParams);
420 result.length = handlers.length;
421 for (var i = 0; i < handlers.length; i++) {
422 var handler = handlers[i];
423 var names = handler.names;
424 var shouldDecodes = handler.shouldDecodes;
425 var params = EmptyObject;
426 var isDynamic = false;
427 if (names !== EmptyArray && shouldDecodes !== EmptyArray) {
428 for (var j = 0; j < names.length; j++) {
429 isDynamic = true;
430 var name = names[j];
431 var capture = captures && captures[currentCapture++];
432 if (params === EmptyObject) {
433 params = {};
434 }
435 if (RouteRecognizer.ENCODE_AND_DECODE_PATH_SEGMENTS && shouldDecodes[j]) {
436 params[name] = capture && decodeURIComponent(capture);
437 }
438 else {
439 params[name] = capture;
440 }
441 }
442 }
443 result[i] = {
444 handler: handler.handler,
445 params: params,
446 isDynamic: isDynamic
447 };
448 }
449 return result;
450}
451function decodeQueryParamPart(part) {
452 // http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1
453 part = part.replace(/\+/gm, "%20");
454 var result;
455 try {
456 result = decodeURIComponent(part);
457 }
458 catch (error) {
459 result = "";
460 }
461 return result;
462}
463var RouteRecognizer = function RouteRecognizer() {
464 this.names = createMap();
465 var states = [];
466 var state = new State(states, 0, -1 /* ANY */, true, false);
467 states[0] = state;
468 this.states = states;
469 this.rootState = state;
470};
471RouteRecognizer.prototype.add = function add (routes, options) {
472 var currentState = this.rootState;
473 var pattern = "^";
474 var types = [0, 0, 0];
475 var handlers = new Array(routes.length);
476 var allSegments = [];
477 var isEmpty = true;
478 var j = 0;
479 for (var i = 0; i < routes.length; i++) {
480 var route = routes[i];
481 var ref = parse(allSegments, route.path, types);
482 var names = ref.names;
483 var shouldDecodes = ref.shouldDecodes;
484 // preserve j so it points to the start of newly added segments
485 for (; j < allSegments.length; j++) {
486 var segment = allSegments[j];
487 if (segment.type === 4 /* Epsilon */) {
488 continue;
489 }
490 isEmpty = false;
491 // Add a "/" for the new segment
492 currentState = currentState.put(47 /* SLASH */, false, false);
493 pattern += "/";
494 // Add a representation of the segment to the NFA and regex
495 currentState = eachChar[segment.type](segment, currentState);
496 pattern += regex[segment.type](segment);
497 }
498 handlers[i] = {
499 handler: route.handler,
500 names: names,
501 shouldDecodes: shouldDecodes
502 };
503 }
504 if (isEmpty) {
505 currentState = currentState.put(47 /* SLASH */, false, false);
506 pattern += "/";
507 }
508 currentState.handlers = handlers;
509 currentState.pattern = pattern + "$";
510 currentState.types = types;
511 var name;
512 if (typeof options === "object" && options !== null && options.as) {
513 name = options.as;
514 }
515 if (name) {
516 // if (this.names[name]) {
517 // throw new Error("You may not add a duplicate route named `" + name + "`.");
518 // }
519 this.names[name] = {
520 segments: allSegments,
521 handlers: handlers
522 };
523 }
524};
525RouteRecognizer.prototype.handlersFor = function handlersFor (name) {
526 var route = this.names[name];
527 if (!route) {
528 throw new Error("There is no route named " + name);
529 }
530 var result = new Array(route.handlers.length);
531 for (var i = 0; i < route.handlers.length; i++) {
532 var handler = route.handlers[i];
533 result[i] = handler;
534 }
535 return result;
536};
537RouteRecognizer.prototype.hasRoute = function hasRoute (name) {
538 return !!this.names[name];
539};
540RouteRecognizer.prototype.generate = function generate$1 (name, params) {
541 var route = this.names[name];
542 var output = "";
543 if (!route) {
544 throw new Error("There is no route named " + name);
545 }
546 var segments = route.segments;
547 for (var i = 0; i < segments.length; i++) {
548 var segment = segments[i];
549 if (segment.type === 4 /* Epsilon */) {
550 continue;
551 }
552 output += "/";
553 output += generate[segment.type](segment, params);
554 }
555 if (output.charAt(0) !== "/") {
556 output = "/" + output;
557 }
558 if (params && params.queryParams) {
559 output += this.generateQueryString(params.queryParams);
560 }
561 return output;
562};
563RouteRecognizer.prototype.generateQueryString = function generateQueryString (params) {
564 var pairs = [];
565 var keys = Object.keys(params);
566 keys.sort();
567 for (var i = 0; i < keys.length; i++) {
568 var key = keys[i];
569 var value = params[key];
570 if (value == null) {
571 continue;
572 }
573 var pair = encodeURIComponent(key);
574 if (isArray(value)) {
575 for (var j = 0; j < value.length; j++) {
576 var arrayPair = key + "[]" + "=" + encodeURIComponent(value[j]);
577 pairs.push(arrayPair);
578 }
579 }
580 else {
581 pair += "=" + encodeURIComponent(value);
582 pairs.push(pair);
583 }
584 }
585 if (pairs.length === 0) {
586 return "";
587 }
588 return "?" + pairs.join("&");
589};
590RouteRecognizer.prototype.parseQueryString = function parseQueryString (queryString) {
591 var pairs = queryString.split("&");
592 var queryParams = {};
593 for (var i = 0; i < pairs.length; i++) {
594 var pair = pairs[i].split("="), key = decodeQueryParamPart(pair[0]), keyLength = key.length, isArray = false, value = (void 0);
595 if (pair.length === 1) {
596 value = "true";
597 }
598 else {
599 // Handle arrays
600 if (keyLength > 2 && key.slice(keyLength - 2) === "[]") {
601 isArray = true;
602 key = key.slice(0, keyLength - 2);
603 if (!queryParams[key]) {
604 queryParams[key] = [];
605 }
606 }
607 value = pair[1] ? decodeQueryParamPart(pair[1]) : "";
608 }
609 if (isArray) {
610 queryParams[key].push(value);
611 }
612 else {
613 queryParams[key] = value;
614 }
615 }
616 return queryParams;
617};
618RouteRecognizer.prototype.recognize = function recognize (path) {
619 var results;
620 var states = [this.rootState];
621 var queryParams = {};
622 var isSlashDropped = false;
623 var hashStart = path.indexOf("#");
624 if (hashStart !== -1) {
625 path = path.substr(0, hashStart);
626 }
627 var queryStart = path.indexOf("?");
628 if (queryStart !== -1) {
629 var queryString = path.substr(queryStart + 1, path.length);
630 path = path.substr(0, queryStart);
631 queryParams = this.parseQueryString(queryString);
632 }
633 if (path.charAt(0) !== "/") {
634 path = "/" + path;
635 }
636 var originalPath = path;
637 if (RouteRecognizer.ENCODE_AND_DECODE_PATH_SEGMENTS) {
638 path = normalizePath(path);
639 }
640 else {
641 path = decodeURI(path);
642 originalPath = decodeURI(originalPath);
643 }
644 var pathLen = path.length;
645 if (pathLen > 1 && path.charAt(pathLen - 1) === "/") {
646 path = path.substr(0, pathLen - 1);
647 originalPath = originalPath.substr(0, originalPath.length - 1);
648 isSlashDropped = true;
649 }
650 for (var i = 0; i < path.length; i++) {
651 states = recognizeChar(states, path.charCodeAt(i));
652 if (!states.length) {
653 break;
654 }
655 }
656 var solutions = [];
657 for (var i$1 = 0; i$1 < states.length; i$1++) {
658 if (states[i$1].handlers) {
659 solutions.push(states[i$1]);
660 }
661 }
662 states = sortSolutions(solutions);
663 var state = solutions[0];
664 if (state && state.handlers) {
665 // if a trailing slash was dropped and a star segment is the last segment
666 // specified, put the trailing slash back
667 if (isSlashDropped && state.pattern && state.pattern.slice(-5) === "(.+)$") {
668 originalPath = originalPath + "/";
669 }
670 results = findHandler(state, originalPath, queryParams);
671 }
672 return results;
673};
674RouteRecognizer.VERSION = "0.3.4";
675// Set to false to opt-out of encoding and decoding path segments.
676// See https://github.com/tildeio/route-recognizer/pull/55
677RouteRecognizer.ENCODE_AND_DECODE_PATH_SEGMENTS = true;
678RouteRecognizer.Normalizer = {
679 normalizeSegment: normalizeSegment, normalizePath: normalizePath, encodePathSegment: encodePathSegment
680};
681RouteRecognizer.prototype.map = map;
682
683export default RouteRecognizer;
684
685//# sourceMappingURL=route-recognizer.es.js.map