reactui
Version:
A components library for ReactJS. This is part of the Gearz project
756 lines (641 loc) • 29.2 kB
JavaScript
/// Gearz Routing v1.0.0
///
/// This is responsible for routing, that is,
/// extracting information from an URI so that
/// an external agent can determine what to
/// render next.
///
/// This will mimic the behaviour of ASP.NET routing with the following exceptions:
/// 1) when building patterns:
/// - none yet
/// 2) when matching URI's:
/// - pattern "{x}-{y}" matches '~/x-y-z/' as:
/// Here: x -> 'x'; y -> 'y-z'
/// ASP.NET: x -> 'x-y'; y -> 'z'
/// - pattern "{x?}-{y}" matches '~/---/' as:
/// Here: x -> no match (x = '' and y = '--', but x is a middle place-holder)
/// ASP.NET: x -> '-'; y -> '-'
///
/// This will not:
/// - change URI for single-page apps
/// - request server data in any way
///
/// This router support mix-in functions, that can be used to add plug-ins into it.
/// With plug-ins, anything is possible, including the above mentioned behaviours.
(function() {
/*****************************************************************************************
** **
** PROTOTYPES. **
** **
*****************************************************************************************/
function RouteError(type, message) {
if (window.chrome) {
var err = new Error();
this.__defineGetter__('stack', function(){return remLine(err.stack, 1);});
this.__defineSetter__('stack', function(value){err.stack=value;});
}
this.message = message;
this.type = type;
}
var freeze = Object.freeze;
var types;
RouteError.prototype = extend(Object.create(Error.prototype), {
message: 'Route error.',
name: 'RouteError',
constructor: RouteError,
types: types = {
SYNTAX_ERROR: 'SYNTAX_ERROR',
EMPTY_SEGMENT: 'EMPTY_SEGMENT',
ADJACENT_PLACEHOLDERS: 'ADJACENT_PLACEHOLDERS',
DUPLICATE_PLACEHOLDER: 'DUPLICATE_PLACEHOLDER',
UNNAMED_PLACEHOLDER: 'UNNAMED_PLACEHOLDER'
}
});
function Literal(value) {
this.value = value.replace(/\{\{/g, "{").replace(/\}\}/g, "}");
this.regexp = escapeRegExp(this.value);
freeze(this);
}
Literal.prototype = {
toString: function() {
return "Literal: " + JSON.stringify(this.value);
}
};
function PlaceHolderBase(name) {
this.name = name;
freeze(this);
}
PlaceHolderBase.prototype = {
toString: function() {
return "Name: " + this.name;
}
};
/*****************************************************************************************
** **
** PRIVATE METHODS. **
** **
*****************************************************************************************/
function extend(target, source) {
for (var k in source)
if (source.hasOwnProperty(k))
target[k] = source[k];
return target;
}
function remLine(str, num) {
var lines = str.split('\n');
lines.splice(num-1, 1);
return lines.join('\n');
}
function getSegments(uriPattern, LiteralClass, PlaceHolderClass) {
var segments = uriPattern && uriPattern.split('/').map(function (seg) {
var ss = seg.split(/(?:((?:[^\{\}]|\{\{|\}\})+)|\{([^\{\}]*)(?!\}\})\})/g),
items = [];
for (var itSs = 0; itSs < ss.length; itSs += 3) {
var empty = ss[itSs],
literal = ss[itSs + 1],
name = ss[itSs + 2];
if (empty) throw new RouteError(types.SYNTAX_ERROR, "Invalid route pattern: near '" + empty + "'");
if (itSs == ss.length - 1) break;
if (typeof literal == 'string') items.push(new LiteralClass(literal));
else if (typeof name == 'string') items.push(new PlaceHolderClass(name));
}
return items;
});
// validating:
// - Names of place-holders cannot be repeated
// - Adjacent place-holders
var usedNames = {},
prevName = '';
for (var itSeg = 0; itSeg < segments.length; itSeg++) {
var subSegs = segments[itSeg];
if (itSeg < segments.length - 1 && subSegs.length == 0)
throw new RouteError(types.EMPTY_SEGMENT, "Invalid route pattern: empty segment #" + itSeg);
for (var itSub = 0; itSub < subSegs.length; itSub++) {
var item = subSegs[itSub];
if (item instanceof PlaceHolderBase) {
if (prevName !== '') throw new RouteError(types.ADJACENT_PLACEHOLDERS, "Invalid route pattern: '{" + prevName + "}' and '{" + item.name + "}' cannot be adjacent");
if (usedNames[item.name]) throw new RouteError(types.DUPLICATE_PLACEHOLDER, "Invalid route pattern: '{" + item.name + "}' used multiple times");
if (!item.name) throw new RouteError(types.UNNAMED_PLACEHOLDER, "Invalid route pattern: found '{}'");
usedNames[item.name] = true;
}
prevName = item instanceof PlaceHolderBase ? item.name : '';
}
prevName = '';
}
return segments;
}
function escapeRegExp(str) {
// http://stackoverflow.com/a/6969486/195417
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}
function matchConstraint(constraint, value) {
// constraint fail
value = value==null ? "" : ""+value;
if (typeof constraint == 'string') {
var regex = new RegExp(constraint, 'g');
if (!regex.test(value))
return false;
}
else if (typeof constraint == 'function') {
if (!constraint(value))
return false;
}
return true;
}
function validateSegmentValues(segments, route, segValues) {
var segIdx = 0,
glbFilled = 0,
glbMissing = 0;
for (var itSeg = 0; itSeg < segments.length; itSeg++) {
var subSegs = segments[itSeg],
missing = 0,
filled = 0,
literals = 0;
for (var itSub = 0; itSub < subSegs.length; itSub++) {
var item = subSegs[itSub],
value = segValues[segIdx++];
if (item instanceof PlaceHolderBase) {
var name = item.name,
constraint = route.Constraints && route.Constraints[name],
def = route.Defaults && route.Defaults[name];
if (!matchConstraint(constraint, value))
return "Match failed: constraint of '{" + name + "}' did not match";
// no value and no default
if (!value && isUndefined(def))
return "Match failed: no value and no default for '{" + name + "}'";
}
else if (item instanceof Literal) {
// ASP.NET: literal can never be missing
if (value !== item.value)
return "Match failed: literal cannot be missing '" + item.value + "'";
literals++;
}
if (!value) missing++;
else filled++;
}
// ASP.NET: segment is partially filled
if (literals && missing)
return "Match failed: segment is partially filled";
// ASP.NET: missing segments may only appear at end
if (!filled) glbMissing++;
else if (glbMissing) {
return "Match failed: missing segments may only appear at end";
}
}
return null;
}
function getRouteValues(route, segments, segValues) {
var segIdx = 0,
r = {};
for (var itSeg = 0; itSeg < segments.length; itSeg++) {
var subSegs = segments[itSeg];
for (var itSub = 0; itSub < subSegs.length; itSub++) {
var item = subSegs[itSub];
if (item instanceof PlaceHolderBase)
r[item.name] = segValues[segIdx];
segIdx++;
}
}
return r;
}
function getSegmentsMatcher(segments) {
var rgxStr = "^";
for (var itSeg = 0; itSeg < segments.length; itSeg++) {
var subSegs = segments[itSeg], cntUnclosed = 0;
rgxStr += "(?:" + (itSeg ? "\\/" : "\\~\\/");
for (var itSub = 0; itSub < subSegs.length; itSub++) {
var item = subSegs[itSub];
if (item instanceof PlaceHolderBase) {
var adjLit = subSegs[itSub + 1],
adjLitRgx = adjLit instanceof Literal ? "|" + adjLit.regexp : "",
op = item.isOptional ? '*' : '+';
rgxStr += "((?:(?!\\/" + adjLitRgx + ").)" + op + ")?";
} else if (item instanceof Literal) {
rgxStr += "(?:" + "(" + item.regexp + ")";
cntUnclosed++;
}
}
for (var itU = 0; itU < cntUnclosed; itU++)
rgxStr += ")?";
}
for (var itP = 0; itP < segments.length; itP++)
rgxStr += ")?";
rgxStr += "$";
var regex = new RegExp(rgxStr, 'g');
SegmentsMatcher.regex = regex;
function SegmentsMatcher(uri) {
regex.lastIndex = 0;
var result = regex.exec(uri);
if (!result) return null;
result.shift();
return result.map(function(s){return s||undefined;});
}
return SegmentsMatcher;
}
function Route(route) {
if (!route || typeof route != 'object')
throw new Error("Invalid route information: route argument cannot be missing");
var constraints = route.Constraints ? extend({}, route.Constraints) : {};
function PlaceHolder(name) {
this.name = name;
this.isOptional = !!route.Defaults
&& route.Defaults.hasOwnProperty(name)
&& !isUndefined(route.Defaults[name]);
if (this.isOptional)
this.defaultValue = route.Defaults[name];
this.isConstrained = constraints.hasOwnProperty(name);
if (this.isConstrained) {
this.constraint = constraints[name];
delete constraints[name];
}
freeze(this);
}
PlaceHolder.prototype = Object.create(PlaceHolderBase.prototype);
var segments = getSegments.call(this, route.UriPattern, Literal, PlaceHolder);
var segmentsMatcher = getSegmentsMatcher.call(this, segments);
// properties with extracted information from the route object
this.segments = segments;
this.match = segmentsMatcher;
this.contextChecks = constraints;
// source object properties
this.UriPattern = route.UriPattern;
this.DataTokens = route.DataTokens;
this.Defaults = route.Defaults;
this.Constraints = route.Constraints;
freeze(this);
}
function freezeAny(o) {
if (typeof o == 'object' && o != null)
return freeze(o);
return o;
}
function RouteMatch(data, tokens, error, details) {
if (!(this instanceof RouteMatch))
throw new Error("Call with 'new' operator.");
this.data = freezeAny(data);
this.tokens = freezeAny(tokens);
this.error = freezeAny(error);
this.details = freezeAny(details);
freeze(this);
}
this.RouteMatch = RouteMatch;
function addParams(p2, values) {
for (var p1 in values) {
if (!this[p1]) this[p1] = {};
this[p1][p2] = values[p1];
}
}
function ifUndef(f,t) {
return isUndefined(f) ? t : f;
}
function isNullOrEmpty(x) {
return x===null||x==="";
}
function isUndefined(x) {
return typeof x == 'undefined';
}
function ensureStringLimits(start, end, str) {
str = ""+str;
end = ""+end;
str = str == "" ? start
: str[0] != start ? (start+str)
: str;
str = str == "" ? end
: str[str.length-1] != end ? (str+end)
: str;
return str;
}
function bindUriValues(route, currentRouteData, targetRouteData, globalValues) {
var params = {};
var add = addParams.bind(params);
add('current', currentRouteData);
add('target', targetRouteData);
add('default', route.Defaults);
add('constraint', route.Constraints);
add('global', globalValues);
// Getting values that will be used.
var result = { uriValues: {}, dataTokens: {} }, allowCurrent = true;
var fnc = false;
for (var itS = 0; itS < route.segments.length; itS++) {
var seg = route.segments[itS];
for (var itSS = 0; itSS < seg.length; itSS++) {
var item = seg[itSS];
if (item instanceof PlaceHolderBase) {
var name = item.name;
if (!params.hasOwnProperty(name))
return null;
var param = params[name];
var c = ifUndef(param.current, g);
var t = param.target;
var d = ifUndef(param.default, g);
// c t d | r action
// -----------+------------
// - - - | stop
// a - - | a
// - a - | a
// - - a | a
// a a - | a
// a b - | b clear c
// a - a | a
// a - b | a
// - a a | a
// - a b | a
// a a a | a
// a a b | a
// a b a | b
// b a a | a
// a b c | b
var nc = !c || fnc,
nt = !t,
nd = isUndefined(d),
ect = c == t || nc && nt,
etd = t == d || nt && nd,
edc = d == c || nd && nc;
var r0;
if (nc && nt && nd ) return null;
else if (!nc && nt && nd ) r0 = c;
else if (nc && !nt && nd ) r0 = t;
else if (nc && nt && !nd) r0 = d;
else if (!nc && ect && nd ) r0 = t;
else if (!nc && !nt && nd ) { r0 = t; fnc = true; }
else if (edc && nt && !nd) r0 = c;
else if (!nc && nt && !nd) r0 = c;
else if (nc && !nt && etd) r0 = t;
else if (nc && !nt && !nd) r0 = t;
else if (edc && ect && !nd) r0 = t;
else if (!nc && ect && !nd) r0 = t;
else if (edc && !nt && !nd) r0 = t;
else if (!nc && !nt && etd) r0 = t;
else if (!nc && !nt && !nd) r0 = t;
param.used = true;
result.uriValues[name] = r0;
r0 = undefined;
}
}
}
// checking remaining parameters
for (var name in params) {
var param = params[name];
if (!param.used) {
var g = param.global;
var c = ifUndef(param.current, g);
var t = param.target;
var d = ifUndef(param.default, g);
// c t d | r action
// -----------+------------
// - - - | -
// a - - | -
// - a - | a
// - - a | stop
// a a - | a
// a b - | b
// a - a | - data-token
// a - b | stop
// - a a | - data-token
// - a b | stop
// a a a | - data-token
// a a b | stop
// a b a | stop
// b a a | - data-token
// a b c | stop
var nc = isUndefined(c),
nt = isUndefined(t),
nd = isUndefined(d),
ect = c == t || nc && nt || isNullOrEmpty(c) && isNullOrEmpty(t),
etd = t == d || nt && nd || isNullOrEmpty(t) && isNullOrEmpty(d),
edc = d == c || nd && nc || isNullOrEmpty(d) && isNullOrEmpty(c);
var r1;
if (nc && nt && nd ) r1 = undefined;
else if (!nc && nt && nd ) r1 = undefined;
else if (nc && !nt && nd ) r1 = t;
else if (nc && nt && !nd) return null;
else if (!nc && ect && nd ) r1 = t;
else if (!nc && !nt && nd ) r1 = t;
else if (edc && nt && !nd) { r1=undefined; result.dataTokens[name]=d; }
else if (!nc && nt && !nd) return null;
else if (nc && !nt && etd) { r1=undefined; result.dataTokens[name]=d; }
else if (nc && !nt && !nd) return null;
else if (edc && ect && !nd) { r1=undefined; result.dataTokens[name]=d; }
else if (!nc && ect && !nd) return null;
else if (edc && !nt && !nd) return null;
else if (!nc && !nt && etd) { r1=undefined; result.dataTokens[name]=d; }
else if (!nc && !nt && !nd) return null;
if (typeof r1 != 'undefined')
{
param.used = true;
result.uriValues[name] = r1;
r1 = undefined;
}
}
}
return result;
}
function buildUri(route, data, basePath) {
var uri = basePath;
var tempUri = uri;
for (var itS = 0; itS < route.segments.length; itS++) {
var seg = route.segments[itS];
tempUri += tempUri[tempUri.length-1] != "/" ? "/" : "";
var segmentRequired = false;
for (var itSS = 0; itSS < seg.length; itSS++) {
var item = seg[itSS];
if (item instanceof PlaceHolderBase) {
var name = item.name,
constraint = route.Constraints && route.Constraints[name],
def = route.Defaults && route.Defaults[name],
value = data.uriValues[name];
// !(A || B) <=> !A && !B
// !(A && B) <=> !A || !B
if (typeof def != 'undefined')
if (def != null && def != "" || value != null && value != "")
if (def != value)
segmentRequired = true;
if (!matchConstraint(constraint, value))
return null;
if (value) tempUri += encodeURIComponent(value);
delete data.uriValues[item.name];
}
else if (item instanceof Literal) {
segmentRequired = true;
tempUri += encodeURIComponent(item.value);
}
}
if (segmentRequired)
uri = tempUri;
}
var sep = '?';
for (var name in data.uriValues) {
var value = data.uriValues[name];
delete data.uriValues[name];
uri += uri[uri.length-1] != sep ? sep : "";
sep = '&';
uri += encodeURIComponent(name) + '=' + encodeURIComponent(value);
}
return uri;
}
function mergeMixin(n, o) {
if (typeof n == 'function') {
return n;
}
}
function appendParam(v0, v1) {
return isUndefined(v0) ? (v1||"")
: isNullOrEmpty(v0) ? ";"+(v1||"")
: v0+";"+(v1||"");
}
function getQueryValues(uri) {
uri = isNullOrEmpty(uri) || isUndefined(uri) ? "" : ""+uri;
uri = uri.replace(new RegExp("^"+escapeRegExp(this.basePath), "g"), "~/");
var qs = uri.split(/[?&]/g);
uri = qs.splice(0,1)[0];
qs = qs.map(function(x){
return decodeURIComponent(x)
.replace(/\+/g, " ")
.replace('=', '&')
.split('&');
});
var qv = {};
for (var it = 0; it < qs.length; it++) {
var kv = qs[it],
name = kv[0];
qv[name] = appendParam(qv[name], kv[1]);
}
return { queryValues: qv, path: uri };
}
function toVirtualPath(path) {
path=""+path;
if (path.indexOf(this.basePath) == 0)
return path.substr(this.basePath.length);
return null;
}
function toAppPath(virtualPath) {
virtualPath=""+virtualPath;
if (virtualPath.indexOf("~/") == 0)
return this.basePath + virtualPath.substr(2);
return null;
}
/*****************************************************************************************
** **
** DEFINITION OF THE ROUTER OBJECT. **
** **
*****************************************************************************************/
function Router(opts) {
if (!(this instanceof Router))
throw new Error("Must call 'Router' with 'new' operator.");
// enclosed values for the following functions
var _routes = [];
// private function definitions, that depend on the above enclosed values
function makeURI(currentRouteData, targetRouteData, opts) {
opts = opts || {};
for (var itR = 0; itR < _routes.length; itR++) {
var route = _routes[itR];
// getting data to use in the URI
var data = bindUriValues.call(
this, route, currentRouteData, targetRouteData, this.globalValues);
// building URI with the data
var uri = null;
if (data) uri = buildUri.call(
this, route, data, opts.virtual ? "~/" : this.basePath);
if (uri) return uri;
}
throw new Error("No matching route to build the URI");
}
function matchRoute(uri, opts) {
var details = opts && opts.verbose ? [] : null;
var parts = getQueryValues.call(this, uri);
// ASP.NET routing code (for reference):
// http://referencesource.microsoft.com/#System.Web/Routing/ParsedRoute.cs
for (var itR = 0; itR < _routes.length; itR++) {
var route = _routes[itR];
// Trying to match the route information with the given URI.
// Convert the URI pattern to a RegExp that can
// extract information from a real URI.
var segments = route.segments;
var segValues = route.match(parts.path);
if (!segValues) {
if (details) details.push("Match failed: URI does not match");
continue;
}
var validation = validateSegmentValues(segments, route, segValues);
if (validation) {
if (details) details.push(validation);
continue;
}
var values = getRouteValues(route, segments, segValues);
var r = {}, t = {};
// Copy route data to the resulting object.
// Copying `DataTokens` to the tokens variable.
if (route.DataTokens)
for (var kt in route.DataTokens)
t[kt] = route.DataTokens[kt];
// Copying the default values to the used values,
// then overriding them with query values,
// and finally overriding them with route data.
if (route.Defaults)
for (var kd in route.Defaults)
r[kd] = route.Defaults[kd];
if (parts.queryValues)
for (var kt in parts.queryValues)
r[kt] = parts.queryValues[kt];
for (var kv in values)
r[kv] = values[kv];
return new RouteMatch(r, t, null, null);
}
return new RouteMatch(null, null, "No routes matched the URI.", details);
}
function addRoute(name, route) {
if (typeof name !== 'string'
&& name != null || name === "" || /^\d+$/.test(name))
throw new Error("Invalid argument: route name is invalid");
_routes.push(new Route(route));
if (name)
_routes[name] = route;
// allow fluent route definitions
return this;
}
function getRoute(idOrName) {
return _routes[idOrName];
}
// values that will be used
var routes = opts.routes,
globalValues = opts.globals,
basePath = opts.basePath,
mixins = opts.mixins || [];
// adding routes
if (routes instanceof Array)
for (var itR = 0; itR < routes.length; itR++)
addRoute.call(this, routes[itR]);
/*****************************************************************************************
** **
** PUBLIC API OF THE ROUTER OBJECT. **
** **
*****************************************************************************************/
// METHODS
this.addRoute = addRoute.bind(this);
this.getRoute = getRoute.bind(this);
this.matchRoute = matchRoute.bind(this);
this.makeURI = makeURI.bind(this);
this.toVirtualPath = toVirtualPath.bind(this);
this.toAppPath = toAppPath.bind(this);
// PROPERTIES
this.globalValues = globalValues || {};
this.basePath =
isUndefined(basePath) ? "~/" :
isNullOrEmpty(basePath) ? "/" :
ensureStringLimits('/', '/', basePath);
this.mixins = mixins;
// APPLYING THE MIX-INS
// Must be the last thing done before freezing.
for (var it = 0; it < mixins.length; it++)
mixins[it].call(this);
freeze(this);
}
/*****************************************************************************************
** **
** PUBLIC GLOBAL DEFINITIONS. **
** **
*****************************************************************************************/
this.RouteError = RouteError;
this.Router = Router;
return Router;
})();