UNPKG

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