2 * crossroads <http://millermedeiros.github.com/crossroads.js/>
4 * Author: Miller Medeiros
5 * Version: 0.11.0 (2012/10/31 21:44)
9 define(['signals'], function (signals) {
15 // Helpers -----------
16 //====================
18 // IE 7-8 capture optional groups as empty strings while other browsers
19 // capture as `undefined`
20 _hasOptionalGroupBug = (/t(.+)?/).exec('t')[1] === '';
22 function arrayIndexOf(arr, val) {
24 return arr.indexOf(val);
26 //Array.indexOf doesn't work on IE 6-7
37 function arrayRemove(arr, item) {
38 var i = arrayIndexOf(arr, item);
44 function isKind(val, kind) {
45 return '[object '+ kind +']' === Object.prototype.toString.call(val);
48 function isRegExp(val) {
49 return isKind(val, 'RegExp');
52 function isArray(val) {
53 return isKind(val, 'Array');
56 function isFunction(val) {
57 return typeof val === 'function';
60 //borrowed from AMD-utils
61 function typecastValue(val) {
63 if (val === null || val === 'null') {
65 } else if (val === 'true') {
67 } else if (val === 'false') {
69 } else if (val === UNDEF || val === 'undefined') {
71 } else if (val === '' || isNaN(val)) {
72 //isNaN('') returns false
75 //parseFloat(null || '') returns NaN
81 function typecastArrayValues(values) {
82 var n = values.length,
85 result[n] = typecastValue(values[n]);
90 //borrowed from AMD-Utils
91 function decodeQueryString(str, shouldTypecast) {
92 var queryArr = (str || '').replace('?', '').split('&'),
97 item = queryArr[n].split('=');
98 val = shouldTypecast ? typecastValue(item[1]) : item[1];
99 obj[item[0]] = (typeof val === 'string')? decodeURIComponent(val) : val;
105 // Crossroads --------
106 //====================
111 function Crossroads() {
112 this.bypassed = new signals.Signal();
113 this.routed = new signals.Signal();
115 this._prevRoutes = [];
120 Crossroads.prototype = {
124 greedyEnabled : true,
130 shouldTypecast : false,
134 resetState : function(){
135 this._prevRoutes.length = 0;
136 this._prevMatchedRequest = null;
137 this._prevBypassedRequest = null;
140 create : function () {
141 return new Crossroads();
144 addRoute : function (pattern, callback, priority) {
145 var route = new Route(pattern, callback, priority, this);
146 this._sortedInsert(route);
150 removeRoute : function (route) {
151 arrayRemove(this._routes, route);
155 removeAllRoutes : function () {
156 var n = this.getNumRoutes();
158 this._routes[n]._destroy();
160 this._routes.length = 0;
163 parse : function (request, defaultArgs) {
164 request = request || '';
165 defaultArgs = defaultArgs || [];
167 // should only care about different requests if ignoreState isn't true
168 if ( !this.ignoreState &&
169 (request === this._prevMatchedRequest ||
170 request === this._prevBypassedRequest) ) {
174 var routes = this._getMatchedRoutes(request),
180 this._prevMatchedRequest = request;
182 this._notifyPrevRoutes(routes, request);
183 this._prevRoutes = routes;
184 //should be incremental loop, execute routes in order
187 cur.route.matched.dispatch.apply(cur.route.matched, defaultArgs.concat(cur.params));
189 this.routed.dispatch.apply(this.routed, defaultArgs.concat([request, cur]));
193 this._prevBypassedRequest = request;
194 this.bypassed.dispatch.apply(this.bypassed, defaultArgs.concat([request]));
197 this._pipeParse(request, defaultArgs);
200 _notifyPrevRoutes : function(matchedRoutes, request) {
202 while (prev = this._prevRoutes[i++]) {
203 //check if switched exist since route may be disposed
204 if(prev.route.switched && this._didSwitch(prev.route, matchedRoutes)) {
205 prev.route.switched.dispatch(request);
210 _didSwitch : function (route, matchedRoutes){
213 while (matched = matchedRoutes[i++]) {
214 // only dispatch switched if it is going to a different route
215 if (matched.route === route) {
222 _pipeParse : function(request, defaultArgs) {
224 while (route = this._piped[i++]) {
225 route.parse(request, defaultArgs);
229 getNumRoutes : function () {
230 return this._routes.length;
233 _sortedInsert : function (route) {
234 //simplified insertion sort
235 var routes = this._routes,
237 do { --n; } while (routes[n] && route._priority <= routes[n]._priority);
238 routes.splice(n+1, 0, route);
241 _getMatchedRoutes : function (request) {
243 routes = this._routes,
246 //should be decrement loop since higher priorities are added at the end of array
247 while (route = routes[--n]) {
248 if ((!res.length || this.greedy || route.greedy) && route.match(request)) {
251 params : route._getParamsArray(request)
254 if (!this.greedyEnabled && res.length) {
261 pipe : function (otherRouter) {
262 this._piped.push(otherRouter);
265 unpipe : function (otherRouter) {
266 arrayRemove(this._piped, otherRouter);
269 toString : function () {
270 return '[crossroads numRoutes:'+ this.getNumRoutes() +']';
275 crossroads = new Crossroads();
276 crossroads.VERSION = '0.11.0';
278 crossroads.NORM_AS_ARRAY = function (req, vals) {
282 crossroads.NORM_AS_OBJECT = function (req, vals) {
287 // Route --------------
288 //=====================
293 function Route(pattern, callback, priority, router) {
294 var isRegexPattern = isRegExp(pattern),
295 patternLexer = crossroads.patternLexer;
296 this._router = router;
297 this._pattern = pattern;
298 this._paramsIds = isRegexPattern? null : patternLexer.getParamIds(pattern);
299 this._optionalParamsIds = isRegexPattern? null : patternLexer.getOptionalParamsIds(pattern);
300 this._matchRegexp = isRegexPattern? pattern : patternLexer.compilePattern(pattern, router.ignoreCase);
301 this.matched = new signals.Signal();
302 this.switched = new signals.Signal();
304 this.matched.add(callback);
306 this._priority = priority || 0;
315 match : function (request) {
316 request = request || '';
317 return this._matchRegexp.test(request) && this._validateParams(request); //validate params even if regexp because of `request_` rule.
320 _validateParams : function (request) {
321 var rules = this.rules,
322 values = this._getParamsObject(request),
325 // normalize_ isn't a validation rule... (#39)
326 if(key !== 'normalize_' && rules.hasOwnProperty(key) && ! this._isValidParam(request, key, values)){
333 _isValidParam : function (request, prop, values) {
334 var validationRule = this.rules[prop],
337 isQuery = (prop.indexOf('?') === 0);
339 if (val == null && this._optionalParamsIds && arrayIndexOf(this._optionalParamsIds, prop) !== -1) {
342 else if (isRegExp(validationRule)) {
344 val = values[prop +'_']; //use raw string
346 isValid = validationRule.test(val);
348 else if (isArray(validationRule)) {
350 val = values[prop +'_']; //use raw string
352 isValid = this._isValidArrayRule(validationRule, val);
354 else if (isFunction(validationRule)) {
355 isValid = validationRule(val, request, values);
358 return isValid; //fail silently if validationRule is from an unsupported type
361 _isValidArrayRule : function (arr, val) {
362 if (! this._router.ignoreCase) {
363 return arrayIndexOf(arr, val) !== -1;
366 if (typeof val === 'string') {
367 val = val.toLowerCase();
376 compareVal = (typeof item === 'string')? item.toLowerCase() : item;
377 if (compareVal === val) {
384 _getParamsObject : function (request) {
385 var shouldTypecast = this._router.shouldTypecast,
386 values = crossroads.patternLexer.getParamValues(request, this._matchRegexp, shouldTypecast),
392 if (this._paramsIds) {
393 param = this._paramsIds[n];
394 if (param.indexOf('?') === 0 && val) {
395 //make a copy of the original string so array and
396 //RegExp validation can be applied properly
398 //update vals_ array as well since it will be used
400 val = decodeQueryString(val, shouldTypecast);
403 // IE will capture optional groups as empty strings while other
404 // browsers will capture `undefined` so normalize behavior.
405 // see: #gh-58, #gh-59, #gh-60
406 if ( _hasOptionalGroupBug && val === '' && arrayIndexOf(this._optionalParamsIds, param) !== -1 ) {
412 //alias to paths and for RegExp pattern
415 o.request_ = shouldTypecast? typecastValue(request) : request;
420 _getParamsArray : function (request) {
421 var norm = this.rules? this.rules.normalize_ : null,
423 norm = norm || this._router.normalizeFn; // default normalize
424 if (norm && isFunction(norm)) {
425 params = norm(request, this._getParamsObject(request));
427 params = this._getParamsObject(request).vals_;
432 interpolate : function(replacements) {
433 var str = crossroads.patternLexer.interpolate(this._pattern, replacements);
434 if (! this._validateParams(str) ) {
435 throw new Error('Generated string doesn\'t validate against `Route.rules`.');
440 dispose : function () {
441 this._router.removeRoute(this);
444 _destroy : function () {
445 this.matched.dispose();
446 this.switched.dispose();
447 this.matched = this.switched = this._pattern = this._matchRegexp = null;
450 toString : function () {
451 return '[Route pattern:"'+ this._pattern +'", numListeners:'+ this.matched.getNumListeners() +']';
458 // Pattern Lexer ------
459 //=====================
461 crossroads.patternLexer = (function () {
464 //match chars that should be escaped on string regexp
465 ESCAPE_CHARS_REGEXP = /[\\.+*?\^$\[\](){}\/'#]/g,
467 //trailing slashes (begin/end of string)
468 LOOSE_SLASHES_REGEXP = /^\/|\/$/g,
469 LEGACY_SLASHES_REGEXP = /\/$/g,
471 //params - everything between `{ }` or `: :`
472 PARAMS_REGEXP = /(?:\{|:)([^}:]+)(?:\}|:)/g,
474 //used to save params during compile (avoid escaping things that
475 //shouldn't be escaped).
479 //slash between `::` or `}:` or `\w:` or `:{?` or `}{?` or `\w{?`
480 rgx : /([:}]|\w(?=\/))\/?(:|(?:\{\?))/g,
486 //used to insert slash between `:{` and `}{`
487 rgx : /([:}])\/?(\{)/g,
492 //required query string - everything in between `{? }`
493 rgx : /\{\?([^}]+)\}/g,
494 //everything from `?` till `#` or end of string
498 //optional query string - everything in between `:? :`
499 rgx : /:\?([^:]+):/g,
500 //everything from `?` till `#` or end of string
501 res : '(?:\\?([^#]*))?'
504 //optional rest - everything in between `: *:`
505 rgx : /:([^:]+)\*:/g,
506 res : '(.*)?' // optional group to avoid passing empty string as captured
509 //rest param - everything in between `{ *}`
510 rgx : /\{([^}]+)\*\}/g,
513 // required/optional params should come after rest segments
515 //required params - everything between `{ }`
516 rgx : /\{([^}]+)\}/g,
520 //optional params - everything between `: :`
522 res : '([^\\/?]+)?\/?'
530 _slashMode = LOOSE_SLASH;
533 function precompileTokens(){
535 for (key in TOKENS) {
536 if (TOKENS.hasOwnProperty(key)) {
538 cur.id = '__CR_'+ key +'__';
539 cur.save = ('save' in cur)? cur.save.replace('{{id}}', cur.id) : cur.id;
540 cur.rRestore = new RegExp(cur.id, 'g');
547 function captureVals(regex, pattern) {
548 var vals = [], match;
549 // very important to reset lastIndex since RegExp can have "g" flag
550 // and multiple runs might affect the result, specially if matching
551 // same string multiple times on IE 7-8
553 while (match = regex.exec(pattern)) {
559 function getParamIds(pattern) {
560 return captureVals(PARAMS_REGEXP, pattern);
563 function getOptionalParamsIds(pattern) {
564 return captureVals(TOKENS.OP.rgx, pattern);
567 function compilePattern(pattern, ignoreCase) {
568 pattern = pattern || '';
571 if (_slashMode === LOOSE_SLASH) {
572 pattern = pattern.replace(LOOSE_SLASHES_REGEXP, '');
574 else if (_slashMode === LEGACY_SLASH) {
575 pattern = pattern.replace(LEGACY_SLASHES_REGEXP, '');
579 pattern = replaceTokens(pattern, 'rgx', 'save');
581 pattern = pattern.replace(ESCAPE_CHARS_REGEXP, '\\$&');
583 pattern = replaceTokens(pattern, 'rRestore', 'res');
585 if (_slashMode === LOOSE_SLASH) {
586 pattern = '\\/?'+ pattern;
590 if (_slashMode !== STRICT_SLASH) {
591 //single slash is treated as empty and end slash is optional
594 return new RegExp('^'+ pattern + '$', ignoreCase? 'i' : '');
597 function replaceTokens(pattern, regexpName, replaceName) {
599 for (key in TOKENS) {
600 if (TOKENS.hasOwnProperty(key)) {
602 pattern = pattern.replace(cur[regexpName], cur[replaceName]);
608 function getParamValues(request, regexp, shouldTypecast) {
609 var vals = regexp.exec(request);
612 if (shouldTypecast) {
613 vals = typecastArrayValues(vals);
619 function interpolate(pattern, replacements) {
620 if (typeof pattern !== 'string') {
621 throw new Error('Route pattern should be a string.');
624 var replaceFn = function(match, prop){
626 if (prop in replacements) {
627 // make sure value is a string see #gh-54
628 val = String(replacements[prop]);
629 if (match.indexOf('*') === -1 && val.indexOf('/') !== -1) {
630 throw new Error('Invalid value "'+ val +'" for segment "'+ match +'".');
633 else if (match.indexOf('{') !== -1) {
634 throw new Error('The segment '+ match +' is required.');
642 if (! TOKENS.OS.trail) {
643 TOKENS.OS.trail = new RegExp('(?:'+ TOKENS.OS.id +')+$');
647 .replace(TOKENS.OS.rgx, TOKENS.OS.save)
648 .replace(PARAMS_REGEXP, replaceFn)
649 .replace(TOKENS.OS.trail, '') // remove trailing
650 .replace(TOKENS.OS.rRestore, '/'); // add slash between segments
656 _slashMode = STRICT_SLASH;
659 _slashMode = LOOSE_SLASH;
662 _slashMode = LEGACY_SLASH;
664 getParamIds : getParamIds,
665 getOptionalParamsIds : getOptionalParamsIds,
666 getParamValues : getParamValues,
667 compilePattern : compilePattern,
668 interpolate : interpolate
676 }(typeof define === 'function' && define.amd ? define : function (deps, factory) {
677 if (typeof module !== 'undefined' && module.exports) { //Node
678 module.exports = factory(require(deps[0]));
681 window['crossroads'] = factory(window[deps[0]]);