628 lines
17 KiB
JavaScript
628 lines
17 KiB
JavaScript
|
(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);
|