123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627 |
- (function(__exports__) {
- "use strict";
- var specials = [
- '/', '.', '*', '+', '?', '|',
- '(', ')', '[', ']', '{', '}', '\\'
- ];
- var escapeRegex = new RegExp('(\\' + specials.join('|\\') + ')', 'g');
- function isArray(test) {
- return Object.prototype.toString.call(test) === "[object Array]";
- }
- // A Segment represents a segment in the original route description.
- // Each Segment type provides an `eachChar` and `regex` method.
- //
- // The `eachChar` method invokes the callback with one or more character
- // specifications. A character specification consumes one or more input
- // characters.
- //
- // The `regex` method returns a regex fragment for the segment. If the
- // segment is a dynamic of star segment, the regex fragment also includes
- // a capture.
- //
- // A character specification contains:
- //
- // * `validChars`: a String with a list of all valid characters, or
- // * `invalidChars`: a String with a list of all invalid characters
- // * `repeat`: true if the character specification can repeat
- function StaticSegment(string) { this.string = string; }
- StaticSegment.prototype = {
- eachChar: function(callback) {
- var string = this.string, ch;
- for (var i=0, l=string.length; i<l; i++) {
- ch = string.charAt(i);
- callback({ validChars: ch });
- }
- },
- regex: function() {
- return this.string.replace(escapeRegex, '\\$1');
- },
- generate: function() {
- return this.string;
- }
- };
- function DynamicSegment(name) { this.name = name; }
- DynamicSegment.prototype = {
- eachChar: function(callback) {
- callback({ invalidChars: "/", repeat: true });
- },
- regex: function() {
- return "([^/]+)";
- },
- generate: function(params) {
- return params[this.name];
- }
- };
- function StarSegment(name) { this.name = name; }
- StarSegment.prototype = {
- eachChar: function(callback) {
- callback({ invalidChars: "", repeat: true });
- },
- regex: function() {
- return "(.+)";
- },
- generate: function(params) {
- return params[this.name];
- }
- };
- function EpsilonSegment() {}
- EpsilonSegment.prototype = {
- eachChar: function() {},
- regex: function() { return ""; },
- generate: function() { return ""; }
- };
- function parse(route, names, types) {
- // normalize route as not starting with a "/". Recognition will
- // also normalize.
- if (route.charAt(0) === "/") { route = route.substr(1); }
- var segments = route.split("/"), results = [];
- for (var i=0, l=segments.length; i<l; i++) {
- var segment = segments[i], match;
- if (match = segment.match(/^:([^\/]+)$/)) {
- results.push(new DynamicSegment(match[1]));
- names.push(match[1]);
- types.dynamics++;
- } else if (match = segment.match(/^\*([^\/]+)$/)) {
- results.push(new StarSegment(match[1]));
- names.push(match[1]);
- types.stars++;
- } else if(segment === "") {
- results.push(new EpsilonSegment());
- } else {
- results.push(new StaticSegment(segment));
- types.statics++;
- }
- }
- return results;
- }
- // A State has a character specification and (`charSpec`) and a list of possible
- // subsequent states (`nextStates`).
- //
- // If a State is an accepting state, it will also have several additional
- // properties:
- //
- // * `regex`: A regular expression that is used to extract parameters from paths
- // that reached this accepting state.
- // * `handlers`: Information on how to convert the list of captures into calls
- // to registered handlers with the specified parameters
- // * `types`: How many static, dynamic or star segments in this route. Used to
- // decide which route to use if multiple registered routes match a path.
- //
- // Currently, State is implemented naively by looping over `nextStates` and
- // comparing a character specification against a character. A more efficient
- // implementation would use a hash of keys pointing at one or more next states.
- function State(charSpec) {
- this.charSpec = charSpec;
- this.nextStates = [];
- }
- State.prototype = {
- get: function(charSpec) {
- var nextStates = this.nextStates;
- for (var i=0, l=nextStates.length; i<l; i++) {
- var child = nextStates[i];
- var isEqual = child.charSpec.validChars === charSpec.validChars;
- isEqual = isEqual && child.charSpec.invalidChars === charSpec.invalidChars;
- if (isEqual) { return child; }
- }
- },
- put: function(charSpec) {
- var state;
- // If the character specification already exists in a child of the current
- // state, just return that state.
- if (state = this.get(charSpec)) { return state; }
- // Make a new state for the character spec
- state = new State(charSpec);
- // Insert the new state as a child of the current state
- this.nextStates.push(state);
- // If this character specification repeats, insert the new state as a child
- // of itself. Note that this will not trigger an infinite loop because each
- // transition during recognition consumes a character.
- if (charSpec.repeat) {
- state.nextStates.push(state);
- }
- // Return the new state
- return state;
- },
- // Find a list of child states matching the next character
- match: function(ch) {
- // DEBUG "Processing `" + ch + "`:"
- var nextStates = this.nextStates,
- child, charSpec, chars;
- // DEBUG " " + debugState(this)
- var returned = [];
- for (var i=0, l=nextStates.length; i<l; i++) {
- child = nextStates[i];
- charSpec = child.charSpec;
- if (typeof (chars = charSpec.validChars) !== 'undefined') {
- if (chars.indexOf(ch) !== -1) { returned.push(child); }
- } else if (typeof (chars = charSpec.invalidChars) !== 'undefined') {
- if (chars.indexOf(ch) === -1) { returned.push(child); }
- }
- }
- return returned;
- }
- /** IF DEBUG
- , debug: function() {
- var charSpec = this.charSpec,
- debug = "[",
- chars = charSpec.validChars || charSpec.invalidChars;
- if (charSpec.invalidChars) { debug += "^"; }
- debug += chars;
- debug += "]";
- if (charSpec.repeat) { debug += "+"; }
- return debug;
- }
- END IF **/
- };
- /** IF DEBUG
- function debug(log) {
- console.log(log);
- }
- function debugState(state) {
- return state.nextStates.map(function(n) {
- if (n.nextStates.length === 0) { return "( " + n.debug() + " [accepting] )"; }
- return "( " + n.debug() + " <then> " + n.nextStates.map(function(s) { return s.debug() }).join(" or ") + " )";
- }).join(", ")
- }
- END IF **/
- // This is a somewhat naive strategy, but should work in a lot of cases
- // A better strategy would properly resolve /posts/:id/new and /posts/edit/:id.
- //
- // This strategy generally prefers more static and less dynamic matching.
- // Specifically, it
- //
- // * prefers fewer stars to more, then
- // * prefers using stars for less of the match to more, then
- // * prefers fewer dynamic segments to more, then
- // * prefers more static segments to more
- function sortSolutions(states) {
- return states.sort(function(a, b) {
- if (a.types.stars !== b.types.stars) { return a.types.stars - b.types.stars; }
- if (a.types.stars) {
- if (a.types.statics !== b.types.statics) { return b.types.statics - a.types.statics; }
- if (a.types.dynamics !== b.types.dynamics) { return b.types.dynamics - a.types.dynamics; }
- }
- if (a.types.dynamics !== b.types.dynamics) { return a.types.dynamics - b.types.dynamics; }
- if (a.types.statics !== b.types.statics) { return b.types.statics - a.types.statics; }
- return 0;
- });
- }
- function recognizeChar(states, ch) {
- var nextStates = [];
- for (var i=0, l=states.length; i<l; i++) {
- var state = states[i];
- nextStates = nextStates.concat(state.match(ch));
- }
- return nextStates;
- }
- var oCreate = Object.create || function(proto) {
- function F() {}
- F.prototype = proto;
- return new F();
- };
- function RecognizeResults(queryParams) {
- this.queryParams = queryParams || {};
- }
- RecognizeResults.prototype = oCreate({
- splice: Array.prototype.splice,
- slice: Array.prototype.slice,
- push: Array.prototype.push,
- length: 0,
- queryParams: null
- });
- function findHandler(state, path, queryParams) {
- var handlers = state.handlers, regex = state.regex;
- var captures = path.match(regex), currentCapture = 1;
- var result = new RecognizeResults(queryParams);
- for (var i=0, l=handlers.length; i<l; i++) {
- var handler = handlers[i], names = handler.names, params = {};
- for (var j=0, m=names.length; j<m; j++) {
- params[names[j]] = captures[currentCapture++];
- }
- result.push({ handler: handler.handler, params: params, isDynamic: !!names.length });
- }
- return result;
- }
- function addSegment(currentState, segment) {
- segment.eachChar(function(ch) {
- var state;
- currentState = currentState.put(ch);
- });
- return currentState;
- }
- // The main interface
- var RouteRecognizer = function() {
- this.rootState = new State();
- this.names = {};
- };
- RouteRecognizer.prototype = {
- add: function(routes, options) {
- var currentState = this.rootState, regex = "^",
- types = { statics: 0, dynamics: 0, stars: 0 },
- handlers = [], allSegments = [], name;
- var isEmpty = true;
- for (var i=0, l=routes.length; i<l; i++) {
- var route = routes[i], names = [];
- var segments = parse(route.path, names, types);
- allSegments = allSegments.concat(segments);
- for (var j=0, m=segments.length; j<m; j++) {
- var segment = segments[j];
- if (segment instanceof EpsilonSegment) { continue; }
- isEmpty = false;
- // Add a "/" for the new segment
- currentState = currentState.put({ validChars: "/" });
- regex += "/";
- // Add a representation of the segment to the NFA and regex
- currentState = addSegment(currentState, segment);
- regex += segment.regex();
- }
- var handler = { handler: route.handler, names: names };
- handlers.push(handler);
- }
- if (isEmpty) {
- currentState = currentState.put({ validChars: "/" });
- regex += "/";
- }
- currentState.handlers = handlers;
- currentState.regex = new RegExp(regex + "$");
- currentState.types = types;
- if (name = options && options.as) {
- this.names[name] = {
- segments: allSegments,
- handlers: handlers
- };
- }
- },
- handlersFor: function(name) {
- var route = this.names[name], result = [];
- if (!route) { throw new Error("There is no route named " + name); }
- for (var i=0, l=route.handlers.length; i<l; i++) {
- result.push(route.handlers[i]);
- }
- return result;
- },
- hasRoute: function(name) {
- return !!this.names[name];
- },
- generate: function(name, params) {
- var route = this.names[name], output = "";
- if (!route) { throw new Error("There is no route named " + name); }
- var segments = route.segments;
- for (var i=0, l=segments.length; i<l; i++) {
- var segment = segments[i];
- if (segment instanceof EpsilonSegment) { continue; }
- output += "/";
- output += segment.generate(params);
- }
- if (output.charAt(0) !== '/') { output = '/' + output; }
- if (params && params.queryParams) {
- output += this.generateQueryString(params.queryParams, route.handlers);
- }
- return output;
- },
- generateQueryString: function(params, handlers) {
- var pairs = [];
- var keys = [];
- for(var key in params) {
- if (params.hasOwnProperty(key)) {
- keys.push(key);
- }
- }
- keys.sort();
- for (var i = 0, len = keys.length; i < len; i++) {
- key = keys[i];
- var value = params[key];
- if (value == null) {
- continue;
- }
- var pair = key;
- if (isArray(value)) {
- for (var j = 0, l = value.length; j < l; j++) {
- var arrayPair = key + '[]' + '=' + encodeURIComponent(value[j]);
- pairs.push(arrayPair);
- }
- } else {
- pair += "=" + encodeURIComponent(value);
- pairs.push(pair);
- }
- }
- if (pairs.length === 0) { return ''; }
- return "?" + pairs.join("&");
- },
- parseQueryString: function(queryString) {
- var pairs = queryString.split("&"), queryParams = {};
- for(var i=0; i < pairs.length; i++) {
- var pair = pairs[i].split('='),
- key = decodeURIComponent(pair[0]),
- keyLength = key.length,
- isArray = false,
- value;
- if (pair.length === 1) {
- value = 'true';
- } else {
- //Handle arrays
- if (keyLength > 2 && key.slice(keyLength -2) === '[]') {
- isArray = true;
- key = key.slice(0, keyLength - 2);
- if(!queryParams[key]) {
- queryParams[key] = [];
- }
- }
- value = pair[1] ? decodeURIComponent(pair[1]) : '';
- }
- if (isArray) {
- queryParams[key].push(value);
- } else {
- queryParams[key] = decodeURIComponent(value);
- }
- }
- return queryParams;
- },
- recognize: function(path) {
- var states = [ this.rootState ],
- pathLen, i, l, queryStart, queryParams = {},
- isSlashDropped = false;
- path = decodeURI(path);
- queryStart = path.indexOf('?');
- if (queryStart !== -1) {
- var queryString = path.substr(queryStart + 1, path.length);
- path = path.substr(0, queryStart);
- queryParams = this.parseQueryString(queryString);
- }
- // DEBUG GROUP path
- if (path.charAt(0) !== "/") { path = "/" + path; }
- pathLen = path.length;
- if (pathLen > 1 && path.charAt(pathLen - 1) === "/") {
- path = path.substr(0, pathLen - 1);
- isSlashDropped = true;
- }
- for (i=0, l=path.length; i<l; i++) {
- states = recognizeChar(states, path.charAt(i));
- if (!states.length) { break; }
- }
- // END DEBUG GROUP
- var solutions = [];
- for (i=0, l=states.length; i<l; i++) {
- if (states[i].handlers) { solutions.push(states[i]); }
- }
- states = sortSolutions(solutions);
- var state = solutions[0];
- if (state && state.handlers) {
- // if a trailing slash was dropped and a star segment is the last segment
- // specified, put the trailing slash back
- if (isSlashDropped && state.regex.source.slice(-5) === "(.+)$") {
- path = path + "/";
- }
- return findHandler(state, path, queryParams);
- }
- }
- };
- __exports__.RouteRecognizer = RouteRecognizer;
- function Target(path, matcher, delegate) {
- this.path = path;
- this.matcher = matcher;
- this.delegate = delegate;
- }
- Target.prototype = {
- to: function(target, callback) {
- var delegate = this.delegate;
- if (delegate && delegate.willAddRoute) {
- target = delegate.willAddRoute(this.matcher.target, target);
- }
- this.matcher.add(this.path, target);
- if (callback) {
- if (callback.length === 0) { throw new Error("You must have an argument in the function passed to `to`"); }
- this.matcher.addChild(this.path, target, callback, this.delegate);
- }
- return this;
- }
- };
- function Matcher(target) {
- this.routes = {};
- this.children = {};
- this.target = target;
- }
- Matcher.prototype = {
- add: function(path, handler) {
- this.routes[path] = handler;
- },
- addChild: function(path, target, callback, delegate) {
- var matcher = new Matcher(target);
- this.children[path] = matcher;
- var match = generateMatch(path, matcher, delegate);
- if (delegate && delegate.contextEntered) {
- delegate.contextEntered(target, match);
- }
- callback(match);
- }
- };
- function generateMatch(startingPath, matcher, delegate) {
- return function(path, nestedCallback) {
- var fullPath = startingPath + path;
- if (nestedCallback) {
- nestedCallback(generateMatch(fullPath, matcher, delegate));
- } else {
- return new Target(startingPath + path, matcher, delegate);
- }
- };
- }
- function addRoute(routeArray, path, handler) {
- var len = 0;
- for (var i=0, l=routeArray.length; i<l; i++) {
- len += routeArray[i].path.length;
- }
- path = path.substr(len);
- var route = { path: path, handler: handler };
- routeArray.push(route);
- }
- function eachRoute(baseRoute, matcher, callback, binding) {
- var routes = matcher.routes;
- for (var path in routes) {
- if (routes.hasOwnProperty(path)) {
- var routeArray = baseRoute.slice();
- addRoute(routeArray, path, routes[path]);
- if (matcher.children[path]) {
- eachRoute(routeArray, matcher.children[path], callback, binding);
- } else {
- callback.call(binding, routeArray);
- }
- }
- }
- }
- RouteRecognizer.prototype.map = function(callback, addRouteCallback) {
- var matcher = new Matcher();
- callback(generateMatch("", matcher, this.delegate));
- eachRoute([], matcher, function(route) {
- if (addRouteCallback) { addRouteCallback(this, route); }
- else { this.add(route); }
- }, this);
- };
- })(window);
|