/** @license * crossroads * Author: Miller Medeiros | MIT License * v0.12.2 (2015/07/31 18:37) */ (function () { var factory = function (signals) { var crossroads, _hasOptionalGroupBug, UNDEF; // Helpers ----------- //==================== // IE 7-8 capture optional groups as empty strings while other browsers // capture as `undefined` _hasOptionalGroupBug = (/t(.+)?/).exec('t')[1] === ''; function arrayIndexOf(arr, val) { if (arr.indexOf) { return arr.indexOf(val); } else { //Array.indexOf doesn't work on IE 6-7 var n = arr.length; while (n--) { if (arr[n] === val) { return n; } } return -1; } } function arrayRemove(arr, item) { var i = arrayIndexOf(arr, item); if (i !== -1) { arr.splice(i, 1); } } function isKind(val, kind) { return '[object '+ kind +']' === Object.prototype.toString.call(val); } function isRegExp(val) { return isKind(val, 'RegExp'); } function isArray(val) { return isKind(val, 'Array'); } function isFunction(val) { return typeof val === 'function'; } //borrowed from AMD-utils function typecastValue(val) { var r; if (val === null || val === 'null') { r = null; } else if (val === 'true') { r = true; } else if (val === 'false') { r = false; } else if (val === UNDEF || val === 'undefined') { r = UNDEF; } else if (val === '' || isNaN(val)) { r = val; } else { r = parseFloat(val); } return r; } function typecastArrayValues(values) { var n = values.length, result = []; while (n--) { result[n] = typecastValue(values[n]); } return result; } // borrowed from MOUT function decodeQueryString(queryStr, shouldTypecast) { var queryArr = (queryStr || '').replace('?', '').split('&'), reg = /([^=]+)=(.+)/, i = -1, obj = {}, equalIndex, cur, pValue, pName; while ((cur = queryArr[++i])) { equalIndex = cur.indexOf('='); pName = cur.substring(0, equalIndex); pValue = decodeURIComponent(cur.substring(equalIndex + 1)); if (shouldTypecast !== false) { pValue = typecastValue(pValue); } if (pName in obj){ if(isArray(obj[pName])){ obj[pName].push(pValue); } else { obj[pName] = [obj[pName], pValue]; } } else { obj[pName] = pValue; } } return obj; } // Crossroads -------- //==================== /** * @constructor */ function Crossroads() { this.bypassed = new signals.Signal(); this.routed = new signals.Signal(); this._routes = []; this._prevRoutes = []; this._piped = []; this.resetState(); } Crossroads.prototype = { greedy : false, greedyEnabled : true, ignoreCase : true, ignoreState : false, shouldTypecast : false, normalizeFn : null, resetState : function(){ this._prevRoutes.length = 0; this._prevMatchedRequest = null; this._prevBypassedRequest = null; }, create : function () { return new Crossroads(); }, addRoute : function (pattern, callback, priority) { var route = new Route(pattern, callback, priority, this); this._sortedInsert(route); return route; }, removeRoute : function (route) { arrayRemove(this._routes, route); route._destroy(); }, removeAllRoutes : function () { var n = this.getNumRoutes(); while (n--) { this._routes[n]._destroy(); } this._routes.length = 0; }, parse : function (request, defaultArgs) { request = request || ''; defaultArgs = defaultArgs || []; // should only care about different requests if ignoreState isn't true if ( !this.ignoreState && (request === this._prevMatchedRequest || request === this._prevBypassedRequest) ) { return; } var routes = this._getMatchedRoutes(request), i = 0, n = routes.length, cur; if (n) { this._prevMatchedRequest = request; this._notifyPrevRoutes(routes, request); this._prevRoutes = routes; //should be incremental loop, execute routes in order while (i < n) { cur = routes[i]; cur.route.matched.dispatch.apply(cur.route.matched, defaultArgs.concat(cur.params)); cur.isFirst = !i; this.routed.dispatch.apply(this.routed, defaultArgs.concat([request, cur])); i += 1; } } else { this._prevBypassedRequest = request; this.bypassed.dispatch.apply(this.bypassed, defaultArgs.concat([request])); } this._pipeParse(request, defaultArgs); }, _notifyPrevRoutes : function(matchedRoutes, request) { var i = 0, prev; while (prev = this._prevRoutes[i++]) { //check if switched exist since route may be disposed if(prev.route.switched && this._didSwitch(prev.route, matchedRoutes)) { prev.route.switched.dispatch(request); } } }, _didSwitch : function (route, matchedRoutes){ var matched, i = 0; while (matched = matchedRoutes[i++]) { // only dispatch switched if it is going to a different route if (matched.route === route) { return false; } } return true; }, _pipeParse : function(request, defaultArgs) { var i = 0, route; while (route = this._piped[i++]) { route.parse(request, defaultArgs); } }, getNumRoutes : function () { return this._routes.length; }, _sortedInsert : function (route) { //simplified insertion sort var routes = this._routes, n = routes.length; do { --n; } while (routes[n] && route._priority <= routes[n]._priority); routes.splice(n+1, 0, route); }, _getMatchedRoutes : function (request) { var res = [], routes = this._routes, n = routes.length, route; //should be decrement loop since higher priorities are added at the end of array while (route = routes[--n]) { if ((!res.length || this.greedy || route.greedy) && route.match(request)) { res.push({ route : route, params : route._getParamsArray(request) }); } if (!this.greedyEnabled && res.length) { break; } } return res; }, pipe : function (otherRouter) { this._piped.push(otherRouter); }, unpipe : function (otherRouter) { arrayRemove(this._piped, otherRouter); }, toString : function () { return '[crossroads numRoutes:'+ this.getNumRoutes() +']'; } }; //"static" instance crossroads = new Crossroads(); crossroads.VERSION = '0.12.2'; crossroads.NORM_AS_ARRAY = function (req, vals) { return [vals.vals_]; }; crossroads.NORM_AS_OBJECT = function (req, vals) { return [vals]; }; // Route -------------- //===================== /** * @constructor */ function Route(pattern, callback, priority, router) { var isRegexPattern = isRegExp(pattern), patternLexer = router.patternLexer; this._router = router; this._pattern = pattern; this._paramsIds = isRegexPattern? null : patternLexer.getParamIds(pattern); this._optionalParamsIds = isRegexPattern? null : patternLexer.getOptionalParamsIds(pattern); this._matchRegexp = isRegexPattern? pattern : patternLexer.compilePattern(pattern, router.ignoreCase); this.matched = new signals.Signal(); this.switched = new signals.Signal(); if (callback) { this.matched.add(callback); } this._priority = priority || 0; } Route.prototype = { greedy : false, rules : void(0), match : function (request) { request = request || ''; return this._matchRegexp.test(request) && this._validateParams(request); //validate params even if regexp because of `request_` rule. }, _validateParams : function (request) { var rules = this.rules, values = this._getParamsObject(request), key; for (key in rules) { // normalize_ isn't a validation rule... (#39) if(key !== 'normalize_' && rules.hasOwnProperty(key) && ! this._isValidParam(request, key, values)){ return false; } } return true; }, _isValidParam : function (request, prop, values) { var validationRule = this.rules[prop], val = values[prop], isValid = false, isQuery = (prop.indexOf('?') === 0); if (val == null && this._optionalParamsIds && arrayIndexOf(this._optionalParamsIds, prop) !== -1) { isValid = true; } else if (isRegExp(validationRule)) { if (isQuery) { val = values[prop +'_']; //use raw string } isValid = validationRule.test(val); } else if (isArray(validationRule)) { if (isQuery) { val = values[prop +'_']; //use raw string } isValid = this._isValidArrayRule(validationRule, val); } else if (isFunction(validationRule)) { isValid = validationRule(val, request, values); } return isValid; //fail silently if validationRule is from an unsupported type }, _isValidArrayRule : function (arr, val) { if (! this._router.ignoreCase) { return arrayIndexOf(arr, val) !== -1; } if (typeof val === 'string') { val = val.toLowerCase(); } var n = arr.length, item, compareVal; while (n--) { item = arr[n]; compareVal = (typeof item === 'string')? item.toLowerCase() : item; if (compareVal === val) { return true; } } return false; }, _getParamsObject : function (request) { var shouldTypecast = this._router.shouldTypecast, values = this._router.patternLexer.getParamValues(request, this._matchRegexp, shouldTypecast), o = {}, n = values.length, param, val; while (n--) { val = values[n]; if (this._paramsIds) { param = this._paramsIds[n]; if (param.indexOf('?') === 0 && val) { //make a copy of the original string so array and //RegExp validation can be applied properly o[param +'_'] = val; //update vals_ array as well since it will be used //during dispatch val = decodeQueryString(val, shouldTypecast); values[n] = val; } // IE will capture optional groups as empty strings while other // browsers will capture `undefined` so normalize behavior. // see: #gh-58, #gh-59, #gh-60 if ( _hasOptionalGroupBug && val === '' && arrayIndexOf(this._optionalParamsIds, param) !== -1 ) { val = void(0); values[n] = val; } o[param] = val; } //alias to paths and for RegExp pattern o[n] = val; } o.request_ = shouldTypecast? typecastValue(request) : request; o.vals_ = values; return o; }, _getParamsArray : function (request) { var norm = this.rules? this.rules.normalize_ : null, params; norm = norm || this._router.normalizeFn; // default normalize if (norm && isFunction(norm)) { params = norm(request, this._getParamsObject(request)); } else { params = this._getParamsObject(request).vals_; } return params; }, interpolate : function(replacements) { var str = this._router.patternLexer.interpolate(this._pattern, replacements); if (! this._validateParams(str) ) { throw new Error('Generated string doesn\'t validate against `Route.rules`.'); } return str; }, dispose : function () { this._router.removeRoute(this); }, _destroy : function () { this.matched.dispose(); this.switched.dispose(); this.matched = this.switched = this._pattern = this._matchRegexp = null; }, toString : function () { return '[Route pattern:"'+ this._pattern +'", numListeners:'+ this.matched.getNumListeners() +']'; } }; // Pattern Lexer ------ //===================== Crossroads.prototype.patternLexer = (function () { var //match chars that should be escaped on string regexp ESCAPE_CHARS_REGEXP = /[\\.+*?\^$\[\](){}\/'#]/g, //trailing slashes (begin/end of string) LOOSE_SLASHES_REGEXP = /^\/|\/$/g, LEGACY_SLASHES_REGEXP = /\/$/g, //params - everything between `{ }` or `: :` PARAMS_REGEXP = /(?:\{|:)([^}:]+)(?:\}|:)/g, //used to save params during compile (avoid escaping things that //shouldn't be escaped). TOKENS = { 'OS' : { //optional slashes //slash between `::` or `}:` or `\w:` or `:{?` or `}{?` or `\w{?` rgx : /([:}]|\w(?=\/))\/?(:|(?:\{\?))/g, save : '$1{{id}}$2', res : '\\/?' }, 'RS' : { //required slashes //used to insert slash between `:{` and `}{` rgx : /([:}])\/?(\{)/g, save : '$1{{id}}$2', res : '\\/' }, 'RQ' : { //required query string - everything in between `{? }` rgx : /\{\?([^}]+)\}/g, //everything from `?` till `#` or end of string res : '\\?([^#]+)' }, 'OQ' : { //optional query string - everything in between `:? :` rgx : /:\?([^:]+):/g, //everything from `?` till `#` or end of string res : '(?:\\?([^#]*))?' }, 'OR' : { //optional rest - everything in between `: *:` rgx : /:([^:]+)\*:/g, res : '(.*)?' // optional group to avoid passing empty string as captured }, 'RR' : { //rest param - everything in between `{ *}` rgx : /\{([^}]+)\*\}/g, res : '(.+)' }, // required/optional params should come after rest segments 'RP' : { //required params - everything between `{ }` rgx : /\{([^}]+)\}/g, res : '([^\\/?]+)' }, 'OP' : { //optional params - everything between `: :` rgx : /:([^:]+):/g, res : '([^\\/?]+)?\/?' } }, LOOSE_SLASH = 1, STRICT_SLASH = 2, LEGACY_SLASH = 3, _slashMode = LOOSE_SLASH; function precompileTokens(){ var key, cur; for (key in TOKENS) { if (TOKENS.hasOwnProperty(key)) { cur = TOKENS[key]; cur.id = '__CR_'+ key +'__'; cur.save = ('save' in cur)? cur.save.replace('{{id}}', cur.id) : cur.id; cur.rRestore = new RegExp(cur.id, 'g'); } } } precompileTokens(); function captureVals(regex, pattern) { var vals = [], match; // very important to reset lastIndex since RegExp can have "g" flag // and multiple runs might affect the result, specially if matching // same string multiple times on IE 7-8 regex.lastIndex = 0; while (match = regex.exec(pattern)) { vals.push(match[1]); } return vals; } function getParamIds(pattern) { return captureVals(PARAMS_REGEXP, pattern); } function getOptionalParamsIds(pattern) { return captureVals(TOKENS.OP.rgx, pattern); } function compilePattern(pattern, ignoreCase) { pattern = pattern || ''; if(pattern){ if (_slashMode === LOOSE_SLASH) { pattern = pattern.replace(LOOSE_SLASHES_REGEXP, ''); } else if (_slashMode === LEGACY_SLASH) { pattern = pattern.replace(LEGACY_SLASHES_REGEXP, ''); } //save tokens pattern = replaceTokens(pattern, 'rgx', 'save'); //regexp escape pattern = pattern.replace(ESCAPE_CHARS_REGEXP, '\\$&'); //restore tokens pattern = replaceTokens(pattern, 'rRestore', 'res'); if (_slashMode === LOOSE_SLASH) { pattern = '\\/?'+ pattern; } } if (_slashMode !== STRICT_SLASH) { //single slash is treated as empty and end slash is optional pattern += '\\/?'; } return new RegExp('^'+ pattern + '$', ignoreCase? 'i' : ''); } function replaceTokens(pattern, regexpName, replaceName) { var cur, key; for (key in TOKENS) { if (TOKENS.hasOwnProperty(key)) { cur = TOKENS[key]; pattern = pattern.replace(cur[regexpName], cur[replaceName]); } } return pattern; } function getParamValues(request, regexp, shouldTypecast) { var vals = regexp.exec(request); if (vals) { vals.shift(); if (shouldTypecast) { vals = typecastArrayValues(vals); } } return vals; } function interpolate(pattern, replacements) { // default to an empty object because pattern might have just // optional arguments replacements = replacements || {}; if (typeof pattern !== 'string') { throw new Error('Route pattern should be a string.'); } var replaceFn = function(match, prop){ var val; prop = (prop.substr(0, 1) === '?')? prop.substr(1) : prop; if (replacements[prop] != null) { if (typeof replacements[prop] === 'object') { var queryParts = [], rep; for(var key in replacements[prop]) { rep = replacements[prop][key]; if (isArray(rep)) { for (var k in rep) { if ( key.slice(-2) == '[]' ) { queryParts.push(encodeURI(key.slice(0, -2)) + '[]=' + encodeURI(rep[k])); } else { queryParts.push(encodeURI(key + '=' + rep[k])); } } } else { queryParts.push(encodeURI(key + '=' + rep)); } } val = '?' + queryParts.join('&'); } else { // make sure value is a string see #gh-54 val = String(replacements[prop]); } if (match.indexOf('*') === -1 && val.indexOf('/') !== -1) { throw new Error('Invalid value "'+ val +'" for segment "'+ match +'".'); } } else if (match.indexOf('{') !== -1) { throw new Error('The segment '+ match +' is required.'); } else { val = ''; } return val; }; if (! TOKENS.OS.trail) { TOKENS.OS.trail = new RegExp('(?:'+ TOKENS.OS.id +')+$'); } return pattern .replace(TOKENS.OS.rgx, TOKENS.OS.save) .replace(PARAMS_REGEXP, replaceFn) .replace(TOKENS.OS.trail, '') // remove trailing .replace(TOKENS.OS.rRestore, '/'); // add slash between segments } //API return { strict : function(){ _slashMode = STRICT_SLASH; }, loose : function(){ _slashMode = LOOSE_SLASH; }, legacy : function(){ _slashMode = LEGACY_SLASH; }, getParamIds : getParamIds, getOptionalParamsIds : getOptionalParamsIds, getParamValues : getParamValues, compilePattern : compilePattern, interpolate : interpolate }; }()); return crossroads; }; if (typeof define === 'function' && define.amd) { define(['signals'], factory); } else if (typeof module !== 'undefined' && module.exports) { //Node module.exports = factory(require('signals')); } else { /*jshint sub:true */ window['crossroads'] = factory(window['signals']); } }());