Skip to content
Browse files

Merge branch 'dev'

* dev:
  only dispatch switched signal if matching a diff route. closes #50
  add crossroads.patternLexer.legacy() for backwards compatibility. see #35
  Revert rest segment validation scheme and add more specs.
  check if interpolated route validate against Route.rules. see #34
  Refactor patternLexer to include interpolate() and change rest params validation.
  add Route.interpolate(). see #34
  add query string support. see #33
  update changelog
  add greedyEnabled setting. closes #46
  add setting to toggle slash behavior. closes #35
  refactor patternLexer to make it easier to add new tokens if necessary
  add global greedy setting. see #46
  • Loading branch information...
2 parents 0cf0d9c + 30199e9 commit 651085d4c1313f25f6e7fa6cac8f915cb1551885 @millermedeiros committed May 28, 2012
View
25 CHANGELOG.md
@@ -1,5 +1,30 @@
# Crossroads.js Changelog #
+
+## Next ##
+
+### API Changes ###
+
+ - added `crossroads.greedy` (#46)
+ - added `crossroads.greedyEnabled` (#46)
+ - added `crossroads.patternLexer.strict()` and
+ `crossroads.patternLexer.loose()` and
+ `crossroads.patternLexer.legacy()` (#35)
+ - added query string support (#33)
+
+### Fixes
+
+ - `Route.switched` is only dispatched if matching a different route. (#50)
+
+### Other
+
+ - change default behavior of slashes at begin/end of request (#35)
+ - query string support affected old segment rules, now `?` is considered as
+ a segment divisor as `/` otherwise optional query string RegExp wouldn't
+ match proper segment if following a required segment. (#33)
+
+
+
## v0.8.0 (2012/03/05) ##
### API Changes ###
View
33 dev/src/crossroads.js
@@ -14,6 +14,10 @@
Crossroads.prototype = {
+ greedy : false,
+
+ greedyEnabled : true,
+
normalizeFn : null,
create : function () {
@@ -54,7 +58,7 @@
cur;
if (n) {
- this._notifyPrevRoutes(request);
+ this._notifyPrevRoutes(routes, request);
this._prevRoutes = routes;
//shold be incremental loop, execute routes in order
while (i < n) {
@@ -69,12 +73,26 @@
}
},
- _notifyPrevRoutes : function(request) {
- var i = 0, cur;
- while (cur = this._prevRoutes[i++]) {
+ _notifyPrevRoutes : function(matchedRoutes, request) {
+ var i = 0, prev;
+ while (prev = this._prevRoutes[i++]) {
//check if switched exist since route may be disposed
- if(cur.route.switched) cur.route.switched.dispatch(request);
+ 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;
},
getNumRoutes : function () {
@@ -96,12 +114,15 @@
route;
//should be decrement loop since higher priorities are added at the end of array
while (route = routes[--n]) {
- if ((!res.length || route.greedy) && route.match(request)) {
+ 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;
},
View
16 dev/src/intro.js
@@ -33,7 +33,7 @@
}
function isFunction(val) {
- return isKind(val, 'Function');
+ return typeof val === 'function';
}
//borrowed from AMD-utils
@@ -65,3 +65,17 @@
}
return result;
}
+
+ //borrowed from AMD-Utils
+ function decodeQueryString(str) {
+ var queryArr = (str || '').replace('?', '').split('&'),
+ n = queryArr.length,
+ obj = {},
+ item, val;
+ while (n--) {
+ item = queryArr[n].split('=');
+ val = typecastValue(item[1]);
+ obj[item[0]] = (typeof val === 'string')? decodeURIComponent(val) : val;
+ }
+ return obj;
+ }
View
196 dev/src/pattern_lexer.js
@@ -4,32 +4,88 @@
crossroads.patternLexer = (function () {
+ var
+ //match chars that should be escaped on string regexp
+ ESCAPE_CHARS_REGEXP = /[\\.+*?\^$\[\](){}\/'#]/g,
- var ESCAPE_CHARS_REGEXP = /[\\.+*?\^$\[\](){}\/'#]/g, //match chars that should be escaped on string regexp
- UNNECESSARY_SLASHES_REGEXP = /\/$/g, //trailing slash
- OPTIONAL_SLASHES_REGEXP = /([:}]|\w(?=\/))\/?(:)/g, //slash between `::` or `}:` or `\w:`. $1 = before, $2 = after
- REQUIRED_SLASHES_REGEXP = /([:}])\/?(\{)/g, //used to insert slash between `:{` and `}{`
+ //trailing slashes (begin/end of string)
+ LOOSE_SLASHES_REGEXP = /^\/|\/$/g,
+ LEGACY_SLASHES_REGEXP = /\/$/g,
- REQUIRED_PARAMS_REGEXP = /\{([^}]+)\}/g, //match everything between `{ }`
- OPTIONAL_PARAMS_REGEXP = /:([^:]+):/g, //match everything between `: :`
- PARAMS_REGEXP = /(?:\{|:)([^}:]+)(?:\}|:)/g, //capture everything between `{ }` or `: :`
- REQUIRED_REST = /\{([^}]+)\*\}/g,
- OPTIONAL_REST = /:([^:]+)\*:/g,
+ //params - everything between `{ }` or `: :`
+ PARAMS_REGEXP = /(?:\{|:)([^}:]+)(?:\}|:)/g,
//used to save params during compile (avoid escaping things that
//shouldn't be escaped).
- SAVE_REQUIRED_PARAMS = '__CR_RP__',
- SAVE_OPTIONAL_PARAMS = '__CR_OP__',
- SAVE_REQUIRED_REST = '__CR_RR__',
- SAVE_OPTIONAL_REST = '__CR_OR__',
- SAVE_REQUIRED_SLASHES = '__CR_RS__',
- SAVE_OPTIONAL_SLASHES = '__CR_OS__',
- SAVED_REQUIRED_REGEXP = new RegExp(SAVE_REQUIRED_PARAMS, 'g'),
- SAVED_OPTIONAL_REGEXP = new RegExp(SAVE_OPTIONAL_PARAMS, 'g'),
- SAVED_REQUIRED_REST_REGEXP = new RegExp(SAVE_REQUIRED_REST, 'g'),
- SAVED_OPTIONAL_REST_REGEXP = new RegExp(SAVE_OPTIONAL_REST, 'g'),
- SAVED_OPTIONAL_SLASHES_REGEXP = new RegExp(SAVE_OPTIONAL_SLASHES, 'g'),
- SAVED_REQUIRED_SLASHES_REGEXP = new RegExp(SAVE_REQUIRED_SLASHES, 'g');
+ 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) {
@@ -45,37 +101,48 @@
}
function getOptionalParamsIds(pattern) {
- return captureVals(OPTIONAL_PARAMS_REGEXP, pattern);
+ return captureVals(TOKENS.OP.rgx, pattern);
}
function compilePattern(pattern) {
pattern = pattern || '';
+
if(pattern){
- pattern = pattern.replace(UNNECESSARY_SLASHES_REGEXP, '');
- pattern = tokenize(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, '\\$&');
- pattern = untokenize(pattern);
+ //restore tokens
+ pattern = replaceTokens(pattern, 'rRestore', 'res');
+
+ if (_slashMode === LOOSE_SLASH) {
+ pattern = '\\/?'+ pattern;
+ }
}
- return new RegExp('^'+ pattern + '/?$'); //trailing slash is optional
- }
- function tokenize(pattern) {
- //save chars that shouldn't be escaped
- pattern = pattern.replace(OPTIONAL_SLASHES_REGEXP, '$1'+ SAVE_OPTIONAL_SLASHES +'$2');
- pattern = pattern.replace(REQUIRED_SLASHES_REGEXP, '$1'+ SAVE_REQUIRED_SLASHES +'$2');
- pattern = pattern.replace(OPTIONAL_REST, SAVE_OPTIONAL_REST);
- pattern = pattern.replace(REQUIRED_REST, SAVE_REQUIRED_REST);
- pattern = pattern.replace(OPTIONAL_PARAMS_REGEXP, SAVE_OPTIONAL_PARAMS);
- return pattern.replace(REQUIRED_PARAMS_REGEXP, SAVE_REQUIRED_PARAMS);
+ if (_slashMode !== STRICT_SLASH) {
+ //single slash is treated as empty and end slash is optional
+ pattern += '\\/?';
+ }
+ return new RegExp('^'+ pattern + '$');
}
- function untokenize(pattern) {
- pattern = pattern.replace(SAVED_OPTIONAL_SLASHES_REGEXP, '\\/?');
- pattern = pattern.replace(SAVED_REQUIRED_SLASHES_REGEXP, '\\/');
- pattern = pattern.replace(SAVED_OPTIONAL_REST_REGEXP, '(.*)?'); // optional group to avoid passing empty string as captured
- pattern = pattern.replace(SAVED_REQUIRED_REST_REGEXP, '(.+)');
- pattern = pattern.replace(SAVED_OPTIONAL_REGEXP, '([^\\/]+)?\/?');
- return pattern.replace(SAVED_REQUIRED_REGEXP, '([^\\/]+)');
+ 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) {
@@ -89,12 +156,55 @@
return vals;
}
+ function interpolate(pattern, replacements) {
+ if (typeof pattern !== 'string') {
+ throw new Error('Route pattern should be a string.');
+ }
+
+ var replaceFn = function(match, prop){
+ var val;
+ if (prop in replacements) {
+ val = 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
+ compilePattern : compilePattern,
+ interpolate : interpolate
};
}());
View
39 dev/src/route.js
@@ -28,6 +28,7 @@
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.
},
@@ -47,15 +48,22 @@
_isValidParam : function (request, prop, values) {
var validationRule = this.rules[prop],
val = values[prop],
- isValid = false;
+ 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 = arrayIndexOf(validationRule, val) !== -1;
}
else if (isFunction(validationRule)) {
@@ -69,12 +77,25 @@
var shouldTypecast = this._router.shouldTypecast,
values = crossroads.patternLexer.getParamValues(request, this._matchRegexp, shouldTypecast),
o = {},
- n = values.length;
+ n = values.length,
+ param, val;
while (n--) {
- o[n] = values[n]; //for RegExp pattern and also alias to normal paths
+ val = values[n];
if (this._paramsIds) {
- o[this._paramsIds[n]] = values[n];
+ 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);
+ 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;
@@ -88,11 +109,19 @@
if (norm && isFunction(norm)) {
params = norm(request, this._getParamsObject(request));
} else {
- params = crossroads.patternLexer.getParamValues(request, this._matchRegexp, this._router.shouldTypecast);
+ params = this._getParamsObject(request).vals_;
}
return params;
},
+ interpolate : function(replacements) {
+ var str = crossroads.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);
},
View
68 dev/tests/spec/interpolate.spec.js
@@ -0,0 +1,68 @@
+/*jshint onevar:false */
+
+//for node
+var crossroads = crossroads || require('crossroads');
+//end node
+
+
+
+describe('Route.interpolate()', function(){
+
+ afterEach(function(){
+ crossroads.removeAllRoutes();
+ });
+
+
+ it('should replace regular segments', function(){
+ var a = crossroads.addRoute('/{foo}/:bar:');
+ expect( a.interpolate({foo: 'lorem', bar: 'ipsum'}) ).toEqual( '/lorem/ipsum' );
+ expect( a.interpolate({foo: 'dolor-sit'}) ).toEqual( '/dolor-sit' );
+ });
+
+ it('should replace rest segments', function(){
+ var a = crossroads.addRoute('lorem/{foo*}:bar*:');
+ expect( a.interpolate({'foo*': 'ipsum/dolor', 'bar*': 'sit/amet'}) ).toEqual( 'lorem/ipsum/dolor/sit/amet' );
+ expect( a.interpolate({'foo*': 'dolor-sit'}) ).toEqual( 'lorem/dolor-sit' );
+ });
+
+ it('should replace multiple optional segments', function(){
+ var a = crossroads.addRoute('lorem/:a::b::c:');
+ expect( a.interpolate({a: 'ipsum', b: 'dolor'}) ).toEqual( 'lorem/ipsum/dolor' );
+ expect( a.interpolate({a: 'ipsum', b: 'dolor', c : 'sit'}) ).toEqual( 'lorem/ipsum/dolor/sit' );
+ expect( a.interpolate({a: 'dolor-sit'}) ).toEqual( 'lorem/dolor-sit' );
+ expect( a.interpolate({}) ).toEqual( 'lorem' );
+ });
+
+ it('should throw an error if missing required argument', function () {
+ var a = crossroads.addRoute('/{foo}/:bar:');
+ expect( function(){
+ a.interpolate({bar: 'ipsum'});
+ }).toThrow( 'The segment {foo} is required.' );
+ });
+
+ it('should throw an error if string doesn\'t match pattern', function(){
+ var a = crossroads.addRoute('/{foo}/:bar:');
+ expect( function(){
+ a.interpolate({foo: 'lorem/ipsum', bar: 'dolor'});
+ }).toThrow( 'Invalid value "lorem/ipsum" for segment "{foo}".' );
+ });
+
+ it('should throw an error if route was created by an RegExp pattern', function () {
+ var a = crossroads.addRoute(/^\w+\/\d+$/);
+ expect( function(){
+ a.interpolate({bar: 'ipsum'});
+ }).toThrow( 'Route pattern should be a string.' );
+ });
+
+ it('should throw an error if generated string doesn\'t validate against rules', function () {
+ var a = crossroads.addRoute('/{foo}/:bar:');
+ a.rules = {
+ foo : ['lorem', 'news'],
+ bar : /^\d+$/
+ };
+ expect( function(){
+ a.interpolate({foo: 'lorem', bar: 'ipsum'});
+ }).toThrow( 'Generated string doesn\'t validate against `Route.rules`.' );
+ });
+
+});
View
243 dev/tests/spec/match.spec.js
@@ -29,35 +29,32 @@ describe('Match', function(){
});
it('should match params', function(){
-
var s = crossroads.addRoute('/{foo}');
expect( s.match('/lorem-ipsum') ).toBe( true );
expect( s.match('/lorem-ipsum/') ).toBe( true );
expect( s.match('/lorem-ipsum/dolor') ).toBe( false );
- expect( s.match('lorem-ipsum') ).toBe( false );
+ expect( s.match('lorem-ipsum') ).toBe( true );
expect( s.match('/123') ).toBe( true );
expect( s.match('/123/') ).toBe( true );
- expect( s.match('123') ).toBe( false );
+ expect( s.match('123') ).toBe( true );
expect( s.match('123/45') ).toBe( false );
-
});
it('should match optional params', function(){
var s = crossroads.addRoute(':bar:');
expect( s.match('lorem-ipsum') ).toBe( true );
expect( s.match('') ).toBe( true );
expect( s.match('lorem-ipsum/dolor') ).toBe( false );
- expect( s.match('/lorem-ipsum/') ).toBe( false );
-
+ expect( s.match('/lorem-ipsum/') ).toBe( true );
});
it('should match normal params and optional params', function(){
var s = crossroads.addRoute('/{foo}/:bar:');
expect( s.match('/lorem-ipsum') ).toBe( true );
expect( s.match('/lorem-ipsum/') ).toBe( true );
expect( s.match('/lorem-ipsum/dolor') ).toBe( true );
- expect( s.match('123/45') ).toBe( false );
+ expect( s.match('123/45') ).toBe( true );
});
it('should work even with optional params on the middle of pattern', function(){
@@ -113,6 +110,99 @@ describe('Match', function(){
});
});
+ describe('query string', function () {
+ it('should match query string as first segment', function () {
+ var r = crossroads.addRoute('{?q}');
+ expect( r.match('') ).toBe( false );
+ expect( r.match('foo') ).toBe( false );
+ expect( r.match('/foo') ).toBe( false );
+ expect( r.match('foo/') ).toBe( false );
+ expect( r.match('/foo/') ).toBe( false );
+ expect( r.match('?foo') ).toBe( true );
+ expect( r.match('?foo=bar') ).toBe( true );
+ expect( r.match('?foo=bar&lorem=123') ).toBe( true );
+ });
+
+ it('should match optional query string as first segment', function () {
+ var r = crossroads.addRoute(':?q:');
+ expect( r.match('') ).toBe( true );
+ expect( r.match('foo') ).toBe( false );
+ expect( r.match('/foo') ).toBe( false );
+ expect( r.match('foo/') ).toBe( false );
+ expect( r.match('/foo/') ).toBe( false );
+ expect( r.match('?foo') ).toBe( true );
+ expect( r.match('?foo=bar') ).toBe( true );
+ expect( r.match('?foo=bar&lorem=123') ).toBe( true );
+ });
+
+ it('should match query string as 2nd segment', function () {
+ var r = crossroads.addRoute('{a}{?q}');
+ expect( r.match('') ).toBe( false );
+ expect( r.match('foo') ).toBe( false );
+ expect( r.match('/foo') ).toBe( false );
+ expect( r.match('foo/') ).toBe( false );
+ expect( r.match('/foo/') ).toBe( false );
+ expect( r.match('foo?foo') ).toBe( true );
+ expect( r.match('foo?foo=bar') ).toBe( true );
+ expect( r.match('foo?foo=bar&lorem=123') ).toBe( true );
+ });
+
+ it('should match optional query string as 2nd segment', function () {
+ var r = crossroads.addRoute('{a}:?q:');
+ expect( r.match('') ).toBe( false );
+ expect( r.match('foo') ).toBe( true );
+ expect( r.match('/foo') ).toBe( true );
+ expect( r.match('foo/') ).toBe( true );
+ expect( r.match('/foo/') ).toBe( true );
+ expect( r.match('foo?foo') ).toBe( true );
+ expect( r.match('foo?foo=bar') ).toBe( true );
+ expect( r.match('foo?foo=bar&lorem=123') ).toBe( true );
+ });
+
+ it('should match query string as middle segment', function () {
+ //if hash is required should use the literal "#" to avoid matching
+ //the last char of string as a string "foo?foo" shouldn't match
+ var r = crossroads.addRoute('{a}{?q}#{hash}');
+ expect( r.match('') ).toBe( false );
+ expect( r.match('foo') ).toBe( false );
+ expect( r.match('/foo') ).toBe( false );
+ expect( r.match('foo/') ).toBe( false );
+ expect( r.match('/foo/') ).toBe( false );
+ expect( r.match('foo?foo') ).toBe( false );
+ expect( r.match('foo?foo#bar') ).toBe( true );
+ expect( r.match('foo?foo=bar#bar') ).toBe( true );
+ expect( r.match('foo?foo=bar&lorem=123#bar') ).toBe( true );
+ });
+
+ it('should match optional query string as middle segment', function () {
+ var r = crossroads.addRoute('{a}:?q::hash:');
+ expect( r.match('') ).toBe( false );
+ expect( r.match('foo') ).toBe( true );
+ expect( r.match('/foo') ).toBe( true );
+ expect( r.match('foo/') ).toBe( true );
+ expect( r.match('/foo/') ).toBe( true );
+ expect( r.match('foo?foo') ).toBe( true );
+ expect( r.match('foo?foo=bar') ).toBe( true );
+ expect( r.match('foo?foo=bar#bar') ).toBe( true );
+ expect( r.match('foo?foo=bar&lorem=123') ).toBe( true );
+ expect( r.match('foo?foo=bar&lorem=123#bar') ).toBe( true );
+ });
+
+ it('should match query string even if not using the special query syntax', function () {
+ var r = crossroads.addRoute('{a}?{q}#{hash}');
+ expect( r.match('') ).toBe( false );
+ expect( r.match('foo') ).toBe( false );
+ expect( r.match('/foo') ).toBe( false );
+ expect( r.match('foo/') ).toBe( false );
+ expect( r.match('/foo/') ).toBe( false );
+ expect( r.match('foo?foo') ).toBe( false );
+ expect( r.match('foo?foo#bar') ).toBe( true );
+ expect( r.match('foo?foo=bar#bar') ).toBe( true );
+ expect( r.match('foo?foo=bar&lorem=123#bar') ).toBe( true );
+ });
+ });
+
+
describe('slash between params are optional', function(){
describe('between required params', function(){
@@ -237,6 +327,108 @@ describe('Match', function(){
});
+ describe('strict slash rules', function () {
+
+ afterEach(function(){
+ crossroads.patternLexer.loose();
+ });
+
+ it('should only match if traling slashes match the original pattern', function () {
+ crossroads.patternLexer.strict();
+
+ var a = crossroads.addRoute('{foo}');
+ expect( a.match('foo') ).toBe( true );
+ expect( a.match('/foo') ).toBe( false );
+ expect( a.match('foo/') ).toBe( false );
+ expect( a.match('/foo/') ).toBe( false );
+
+ var b = crossroads.addRoute('/{foo}');
+ expect( b.match('foo') ).toBe( false );
+ expect( b.match('/foo') ).toBe( true );
+ expect( b.match('foo/') ).toBe( false );
+ expect( b.match('/foo/') ).toBe( false );
+
+ var c = crossroads.addRoute('');
+ expect( c.match() ).toBe( true );
+ expect( c.match('') ).toBe( true );
+ expect( c.match('/') ).toBe( false );
+ expect( c.match('foo') ).toBe( false );
+
+ var d = crossroads.addRoute('/');
+ expect( d.match() ).toBe( false );
+ expect( d.match('') ).toBe( false );
+ expect( d.match('/') ).toBe( true );
+ expect( d.match('foo') ).toBe( false );
+ });
+
+ });
+
+
+ describe('loose slash rules', function () {
+
+ beforeEach(function(){
+ crossroads.patternLexer.loose();
+ });
+
+ it('should treat single slash and empty string as same', function () {
+ var c = crossroads.addRoute('');
+ expect( c.match() ).toBe( true );
+ expect( c.match('') ).toBe( true );
+ expect( c.match('/') ).toBe( true );
+ expect( c.match('foo') ).toBe( false );
+
+ var d = crossroads.addRoute('/');
+ expect( d.match() ).toBe( true );
+ expect( d.match('') ).toBe( true );
+ expect( d.match('/') ).toBe( true );
+ expect( d.match('foo') ).toBe( false );
+ });
+
+ });
+
+ describe('legacy slash rules', function () {
+
+ beforeEach(function(){
+ crossroads.patternLexer.legacy();
+ });
+
+ afterEach(function(){
+ crossroads.patternLexer.loose();
+ });
+
+ it('should treat single slash and empty string as same', function () {
+ var c = crossroads.addRoute('');
+ expect( c.match() ).toBe( true );
+ expect( c.match('') ).toBe( true );
+ expect( c.match('/') ).toBe( true );
+ expect( c.match('foo') ).toBe( false );
+
+ var d = crossroads.addRoute('/');
+ expect( d.match() ).toBe( true );
+ expect( d.match('') ).toBe( true );
+ expect( d.match('/') ).toBe( true );
+ expect( d.match('foo') ).toBe( false );
+ });
+
+ it('slash at end of string is optional', function () {
+ var a = crossroads.addRoute('/foo');
+ expect( a.match('/foo') ).toEqual( true );
+ expect( a.match('/foo/') ).toEqual( true );
+ expect( a.match('/foo/bar') ).toEqual( false );
+ });
+
+ it('slash at begin of string is required', function () {
+ var a = crossroads.addRoute('/foo');
+ expect( a.match('/foo') ).toEqual( true );
+ expect( a.match('/foo/') ).toEqual( true );
+ expect( a.match('foo') ).toEqual( false );
+ expect( a.match('foo/') ).toEqual( false );
+ expect( a.match('/foo/bar') ).toEqual( false );
+ });
+
+ });
+
+
describe('rules', function(){
describe('basic rules', function(){
@@ -370,6 +562,43 @@ describe('Match', function(){
});
+ describe('query string', function () {
+
+ it('should validate with array', function () {
+ var r = crossroads.addRoute('/foo.php{?query}');
+ r.rules = {
+ '?query' : ['lorem=ipsum&dolor=456', 'amet=789']
+ };
+ expect( r.match('foo.php?bar=123&ipsum=dolor') ).toBe( false );
+ expect( r.match('foo.php?lorem=ipsum&dolor=456') ).toBe( true );
+ expect( r.match('foo.php?amet=789') ).toBe( true );
+ });
+
+ it('should validate with RegExp', function () {
+ var r = crossroads.addRoute('/foo.php{?query}');
+ r.rules = {
+ '?query' : /^lorem=\w+&dolor=\d+$/
+ };
+ expect( r.match('foo.php?bar=123&ipsum=dolor') ).toBe( false );
+ expect( r.match('foo.php?lorem=ipsum&dolor=12345') ).toBe( true );
+ expect( r.match('foo.php?lorem=ipsum&dolor=amet') ).toBe( false );
+ });
+
+ it('should validate with Function', function () {
+ var r = crossroads.addRoute('/foo.php{?query}');
+ r.rules = {
+ '?query' : function(val, req, vals){
+ return (val.lorem === 'ipsum' && typeof val.dolor === 'number');
+ }
+ };
+ expect( r.match('foo.php?bar=123&ipsum=dolor') ).toBe( false );
+ expect( r.match('foo.php?lorem=ipsum&dolor=12345') ).toBe( true );
+ expect( r.match('foo.php?lorem=ipsum&dolor=amet') ).toBe( false );
+ });
+
+ });
+
+
describe('path alias', function(){
it('should work with string pattern', function(){
View
204 dev/tests/spec/parse.spec.js
@@ -28,7 +28,7 @@ describe('crossroads.parse()', function(){
crossroads.parse('/foo');
crossroads.parse('foo');
- expect( t1 ).toBe( 1 );
+ expect( t1 ).toBe( 2 );
});
it('should pass params and allow multiple routes', function(){
@@ -660,6 +660,96 @@ describe('crossroads.parse()', function(){
});
+ it('should allow global greedy setting', function () {
+
+ var t1, t2, t3, t4, t5, t6, t7, t8;
+
+ crossroads.greedy = true;
+
+ var r1 = crossroads.addRoute('/{a}/{b}/', function(a,b){
+ t1 = a;
+ t2 = b;
+ });
+
+ var r2 = crossroads.addRoute('/bar/{b}/', function(a,b){
+ t3 = a;
+ t4 = b;
+ });
+
+ var r3 = crossroads.addRoute('/foo/{b}/', function(a,b){
+ t5 = a;
+ t6 = b;
+ });
+
+ var r4 = crossroads.addRoute('/{a}/:b:/', function(a,b){
+ t7 = a;
+ t8 = b;
+ });
+
+ crossroads.parse('/foo/lorem');
+
+ expect( t1 ).toEqual( 'foo' );
+ expect( t2 ).toEqual( 'lorem' );
+ expect( t3 ).toBeUndefined();
+ expect( t4 ).toBeUndefined();
+ expect( t5 ).toEqual( 'lorem' );
+ expect( t6 ).toBeUndefined();
+ expect( t7 ).toEqual( 'foo' );
+ expect( t8 ).toEqual( 'lorem' );
+
+ crossroads.greedy = false;
+
+ });
+
+ describe('greedyEnabled', function () {
+
+ afterEach(function(){
+ crossroads.greedyEnabled = true;
+ });
+
+ it('should toggle greedy behavior', function () {
+ crossroads.greedyEnabled = false;
+
+ var t1, t2, t3, t4, t5, t6, t7, t8;
+
+ var r1 = crossroads.addRoute('/{a}/{b}/', function(a,b){
+ t1 = a;
+ t2 = b;
+ });
+ r1.greedy = false;
+
+ var r2 = crossroads.addRoute('/bar/{b}/', function(a,b){
+ t3 = a;
+ t4 = b;
+ });
+ r2.greedy = true;
+
+ var r3 = crossroads.addRoute('/foo/{b}/', function(a,b){
+ t5 = a;
+ t6 = b;
+ });
+ r3.greedy = true;
+
+ var r4 = crossroads.addRoute('/{a}/:b:/', function(a,b){
+ t7 = a;
+ t8 = b;
+ });
+ r4.greedy = true;
+
+ crossroads.parse('/foo/lorem');
+
+ expect( t1 ).toEqual( 'foo' );
+ expect( t2 ).toEqual( 'lorem' );
+ expect( t3 ).toBeUndefined();
+ expect( t4 ).toBeUndefined();
+ expect( t5 ).toBeUndefined();
+ expect( t6 ).toBeUndefined();
+ expect( t7 ).toBeUndefined();
+ expect( t8 ).toBeUndefined();
+ });
+
+ });
+
});
describe('default arguments', function () {
@@ -794,4 +884,116 @@ describe('crossroads.parse()', function(){
});
+ describe('query string', function () {
+
+ describe('old syntax', function () {
+ it('should only parse query string if using special capturing group', function () {
+ var r = crossroads.addRoute('{a}?{q}#{hash}');
+ var t1, t2, t3;
+ r.matched.addOnce(function(a, b, c){
+ t1 = a;
+ t2 = b;
+ t3 = c;
+ });
+ crossroads.parse('foo.php?foo=bar&lorem=123#bar');
+
+ expect( t1 ).toEqual( 'foo.php' );
+ expect( t2 ).toEqual( 'foo=bar&lorem=123' );
+ expect( t3 ).toEqual( 'bar' );
+ });
+ });
+
+ describe('required query string after required segment', function () {
+ it('should parse query string into an object and typecast vals', function () {
+ var r = crossroads.addRoute('{a}{?b}');
+ var t1, t2;
+ r.matched.addOnce(function(a, b){
+ t1 = a;
+ t2 = b;
+ });
+ crossroads.parse('foo.php?lorem=ipsum&asd=123&bar=false');
+
+ expect( t1 ).toEqual( 'foo.php' );
+ expect( t2 ).toEqual( {lorem : 'ipsum', asd : 123, bar : false} );
+ });
+ });
+
+ describe('required query string after optional segment', function () {
+ it('should parse query string into an object and typecast vals', function () {
+ var r = crossroads.addRoute(':a:{?b}');
+ var t1, t2;
+ r.matched.addOnce(function(a, b){
+ t1 = a;
+ t2 = b;
+ });
+ crossroads.parse('foo.php?lorem=ipsum&asd=123&bar=false');
+
+ expect( t1 ).toEqual( 'foo.php' );
+ expect( t2 ).toEqual( {lorem : 'ipsum', asd : 123, bar : false} );
+
+ var t3, t4;
+ r.matched.addOnce(function(a, b){
+ t3 = a;
+ t4 = b;
+ });
+ crossroads.parse('?lorem=ipsum&asd=123');
+
+ expect( t3 ).toBeUndefined();
+ expect( t4 ).toEqual( {lorem : 'ipsum', asd : 123} );
+ });
+ });
+
+ describe('optional query string after required segment', function () {
+ it('should parse query string into an object and typecast vals', function () {
+ var r = crossroads.addRoute('{a}:?b:');
+ var t1, t2;
+ r.matched.addOnce(function(a, b){
+ t1 = a;
+ t2 = b;
+ });
+ crossroads.parse('foo.php?lorem=ipsum&asd=123&bar=false');
+
+ expect( t1 ).toEqual( 'foo.php' );
+ expect( t2 ).toEqual( {lorem : 'ipsum', asd : 123, bar : false} );
+
+ var t3, t4;
+ r.matched.addOnce(function(a, b){
+ t3 = a;
+ t4 = b;
+ });
+ crossroads.parse('bar.php');
+
+ expect( t3 ).toEqual( 'bar.php' );
+ expect( t4 ).toBeUndefined();
+ });
+ });
+
+ describe('optional query string after optional segment', function () {
+ it('should parse query string into an object and typecast vals', function () {
+ var r = crossroads.addRoute(':a::?b:');
+ var t1, t2;
+ r.matched.addOnce(function(a, b){
+ t1 = a;
+ t2 = b;
+ });
+ crossroads.parse('foo.php?lorem=ipsum&asd=123&bar=false');
+
+ expect( t1 ).toEqual( 'foo.php' );
+ expect( t2 ).toEqual( {lorem : 'ipsum', asd : 123, bar : false} );
+
+ var t3, t4;
+ r.matched.addOnce(function(a, b){
+ t3 = a;
+ t4 = b;
+ });
+ crossroads.parse('bar.php');
+
+ expect( t3 ).toEqual( 'bar.php' );
+ expect( t4 ).toBeUndefined();
+ });
+ });
+
+ });
+
+
});
View
9 dev/tests/spec/signals.spec.js
@@ -115,7 +115,7 @@ describe('crossroads Signals', function(){
});
r1.switched.add(function(r){
- vals.push('lorem'); //make sure happened before next matched
+ vals.push('SWITCH'); //make sure happened before next matched
req = r;
count += 1;
});
@@ -125,11 +125,14 @@ describe('crossroads Signals', function(){
count += 1;
});
+ // matching same route twice shouldn't trigger a switched signal (#50)
crossroads.parse('/foo');
+ crossroads.parse('/dolor');
+
crossroads.parse('/foo/bar');
- expect( count ).toBe( 3 );
- expect( vals ).toEqual( ['foo', 'lorem', 'bar'] );
+ expect( count ).toBe( 4 );
+ expect( vals ).toEqual( ['foo', 'dolor', 'SWITCH', 'bar'] );
expect( req ).toEqual( '/foo/bar' );
});
View
1 dev/tests/spec_runner-dev.html
@@ -20,6 +20,7 @@
<script src="spec/create.spec.js"></script>
<script src="spec/signals.spec.js"></script>
<script src="spec/toString.spec.js"></script>
+ <script src="spec/interpolate.spec.js"></script>
<script src="spec/dispose.spec.js"></script>
</head>
<body>
View
1 dev/tests/spec_runner-dist.html
@@ -17,6 +17,7 @@
<script src="spec/create.spec.js"></script>
<script src="spec/signals.spec.js"></script>
<script src="spec/toString.spec.js"></script>
+ <script src="spec/interpolate.spec.js"></script>
<script src="spec/dispose.spec.js"></script>
</head>
<body>
View
288 dist/crossroads.js
@@ -2,7 +2,7 @@
* crossroads <http://millermedeiros.github.com/crossroads.js/>
* License: MIT
* Author: Miller Medeiros
- * Version: 0.8.0 (2012/3/5 14:26)
+ * Version: 0.9.0-alpha (2012/5/28 22:54)
*/
(function (define) {
@@ -42,7 +42,7 @@ define(['signals'], function (signals) {
}
function isFunction(val) {
- return isKind(val, 'Function');
+ return typeof val === 'function';
}
//borrowed from AMD-utils
@@ -75,6 +75,20 @@ define(['signals'], function (signals) {
return result;
}
+ //borrowed from AMD-Utils
+ function decodeQueryString(str) {
+ var queryArr = (str || '').replace('?', '').split('&'),
+ n = queryArr.length,
+ obj = {},
+ item, val;
+ while (n--) {
+ item = queryArr[n].split('=');
+ val = typecastValue(item[1]);
+ obj[item[0]] = (typeof val === 'string')? decodeURIComponent(val) : val;
+ }
+ return obj;
+ }
+
// Crossroads --------
//====================
@@ -91,6 +105,10 @@ define(['signals'], function (signals) {
Crossroads.prototype = {
+ greedy : false,
+
+ greedyEnabled : true,
+
normalizeFn : null,
create : function () {
@@ -131,7 +149,7 @@ define(['signals'], function (signals) {
cur;
if (n) {
- this._notifyPrevRoutes(request);
+ this._notifyPrevRoutes(routes, request);
this._prevRoutes = routes;
//shold be incremental loop, execute routes in order
while (i < n) {
@@ -146,14 +164,28 @@ define(['signals'], function (signals) {
}
},
- _notifyPrevRoutes : function(request) {
- var i = 0, cur;
- while (cur = this._prevRoutes[i++]) {
+ _notifyPrevRoutes : function(matchedRoutes, request) {
+ var i = 0, prev;
+ while (prev = this._prevRoutes[i++]) {
//check if switched exist since route may be disposed
- if(cur.route.switched) cur.route.switched.dispatch(request);
+ 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;
+ },
+
getNumRoutes : function () {
return this._routes.length;
},
@@ -173,12 +205,15 @@ define(['signals'], function (signals) {
route;
//should be decrement loop since higher priorities are added at the end of array
while (route = routes[--n]) {
- if ((!res.length || route.greedy) && route.match(request)) {
+ 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;
},
@@ -190,7 +225,7 @@ define(['signals'], function (signals) {
//"static" instance
crossroads = new Crossroads();
- crossroads.VERSION = '0.8.0';
+ crossroads.VERSION = '0.9.0-alpha';
crossroads.NORM_AS_ARRAY = function (req, vals) {
return [vals.vals_];
@@ -230,6 +265,7 @@ define(['signals'], function (signals) {
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.
},
@@ -249,15 +285,22 @@ define(['signals'], function (signals) {
_isValidParam : function (request, prop, values) {
var validationRule = this.rules[prop],
val = values[prop],
- isValid = false;
+ 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 = arrayIndexOf(validationRule, val) !== -1;
}
else if (isFunction(validationRule)) {
@@ -271,12 +314,25 @@ define(['signals'], function (signals) {
var shouldTypecast = this._router.shouldTypecast,
values = crossroads.patternLexer.getParamValues(request, this._matchRegexp, shouldTypecast),
o = {},
- n = values.length;
+ n = values.length,
+ param, val;
while (n--) {
- o[n] = values[n]; //for RegExp pattern and also alias to normal paths
+ val = values[n];
if (this._paramsIds) {
- o[this._paramsIds[n]] = values[n];
+ 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);
+ 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;
@@ -290,11 +346,19 @@ define(['signals'], function (signals) {
if (norm && isFunction(norm)) {
params = norm(request, this._getParamsObject(request));
} else {
- params = crossroads.patternLexer.getParamValues(request, this._matchRegexp, this._router.shouldTypecast);
+ params = this._getParamsObject(request).vals_;
}
return params;
},
+ interpolate : function(replacements) {
+ var str = crossroads.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);
},
@@ -318,32 +382,88 @@ define(['signals'], function (signals) {
crossroads.patternLexer = (function () {
+ var
+ //match chars that should be escaped on string regexp
+ ESCAPE_CHARS_REGEXP = /[\\.+*?\^$\[\](){}\/'#]/g,
- var ESCAPE_CHARS_REGEXP = /[\\.+*?\^$\[\](){}\/'#]/g, //match chars that should be escaped on string regexp
- UNNECESSARY_SLASHES_REGEXP = /\/$/g, //trailing slash
- OPTIONAL_SLASHES_REGEXP = /([:}]|\w(?=\/))\/?(:)/g, //slash between `::` or `}:` or `\w:`. $1 = before, $2 = after
- REQUIRED_SLASHES_REGEXP = /([:}])\/?(\{)/g, //used to insert slash between `:{` and `}{`
+ //trailing slashes (begin/end of string)
+ LOOSE_SLASHES_REGEXP = /^\/|\/$/g,
+ LEGACY_SLASHES_REGEXP = /\/$/g,
- REQUIRED_PARAMS_REGEXP = /\{([^}]+)\}/g, //match everything between `{ }`
- OPTIONAL_PARAMS_REGEXP = /:([^:]+):/g, //match everything between `: :`
- PARAMS_REGEXP = /(?:\{|:)([^}:]+)(?:\}|:)/g, //capture everything between `{ }` or `: :`
- REQUIRED_REST = /\{([^}]+)\*\}/g,
- OPTIONAL_REST = /:([^:]+)\*:/g,
+ //params - everything between `{ }` or `: :`
+ PARAMS_REGEXP = /(?:\{|:)([^}:]+)(?:\}|:)/g,
//used to save params during compile (avoid escaping things that
//shouldn't be escaped).
- SAVE_REQUIRED_PARAMS = '__CR_RP__',
- SAVE_OPTIONAL_PARAMS = '__CR_OP__',
- SAVE_REQUIRED_REST = '__CR_RR__',
- SAVE_OPTIONAL_REST = '__CR_OR__',
- SAVE_REQUIRED_SLASHES = '__CR_RS__',
- SAVE_OPTIONAL_SLASHES = '__CR_OS__',
- SAVED_REQUIRED_REGEXP = new RegExp(SAVE_REQUIRED_PARAMS, 'g'),
- SAVED_OPTIONAL_REGEXP = new RegExp(SAVE_OPTIONAL_PARAMS, 'g'),
- SAVED_REQUIRED_REST_REGEXP = new RegExp(SAVE_REQUIRED_REST, 'g'),
- SAVED_OPTIONAL_REST_REGEXP = new RegExp(SAVE_OPTIONAL_REST, 'g'),
- SAVED_OPTIONAL_SLASHES_REGEXP = new RegExp(SAVE_OPTIONAL_SLASHES, 'g'),
- SAVED_REQUIRED_SLASHES_REGEXP = new RegExp(SAVE_REQUIRED_SLASHES, 'g');
+ 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) {
@@ -359,37 +479,48 @@ define(['signals'], function (signals) {
}
function getOptionalParamsIds(pattern) {
- return captureVals(OPTIONAL_PARAMS_REGEXP, pattern);
+ return captureVals(TOKENS.OP.rgx, pattern);
}
function compilePattern(pattern) {
pattern = pattern || '';
+
if(pattern){
- pattern = pattern.replace(UNNECESSARY_SLASHES_REGEXP, '');
- pattern = tokenize(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, '\\$&');
- pattern = untokenize(pattern);
+ //restore tokens
+ pattern = replaceTokens(pattern, 'rRestore', 'res');
+
+ if (_slashMode === LOOSE_SLASH) {
+ pattern = '\\/?'+ pattern;
+ }
}
- return new RegExp('^'+ pattern + '/?$'); //trailing slash is optional
- }
- function tokenize(pattern) {
- //save chars that shouldn't be escaped
- pattern = pattern.replace(OPTIONAL_SLASHES_REGEXP, '$1'+ SAVE_OPTIONAL_SLASHES +'$2');
- pattern = pattern.replace(REQUIRED_SLASHES_REGEXP, '$1'+ SAVE_REQUIRED_SLASHES +'$2');
- pattern = pattern.replace(OPTIONAL_REST, SAVE_OPTIONAL_REST);
- pattern = pattern.replace(REQUIRED_REST, SAVE_REQUIRED_REST);
- pattern = pattern.replace(OPTIONAL_PARAMS_REGEXP, SAVE_OPTIONAL_PARAMS);
- return pattern.replace(REQUIRED_PARAMS_REGEXP, SAVE_REQUIRED_PARAMS);
+ if (_slashMode !== STRICT_SLASH) {
+ //single slash is treated as empty and end slash is optional
+ pattern += '\\/?';
+ }
+ return new RegExp('^'+ pattern + '$');
}
- function untokenize(pattern) {
- pattern = pattern.replace(SAVED_OPTIONAL_SLASHES_REGEXP, '\\/?');
- pattern = pattern.replace(SAVED_REQUIRED_SLASHES_REGEXP, '\\/');
- pattern = pattern.replace(SAVED_OPTIONAL_REST_REGEXP, '(.*)?'); // optional group to avoid passing empty string as captured
- pattern = pattern.replace(SAVED_REQUIRED_REST_REGEXP, '(.+)');
- pattern = pattern.replace(SAVED_OPTIONAL_REGEXP, '([^\\/]+)?\/?');
- return pattern.replace(SAVED_REQUIRED_REGEXP, '([^\\/]+)');
+ 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) {
@@ -403,12 +534,55 @@ define(['signals'], function (signals) {
return vals;
}
+ function interpolate(pattern, replacements) {
+ if (typeof pattern !== 'string') {
+ throw new Error('Route pattern should be a string.');
+ }
+
+ var replaceFn = function(match, prop){
+ var val;
+ if (prop in replacements) {
+ val = 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
+ compilePattern : compilePattern,
+ interpolate : interpolate
};
}());
View
4 dist/crossroads.min.js
@@ -2,6 +2,6 @@
* crossroads <http://millermedeiros.github.com/crossroads.js/>
* License: MIT
* Author: Miller Medeiros
- * Version: 0.8.0 (2012/3/5 14:26)
+ * Version: 0.9.0-alpha (2012/5/28 22:54)
*/
-(function(a){a(["signals"],function(a){function d(a,b){if(a.indexOf)return a.indexOf(b);var c=a.length;while(c--)if(a[c]===b)return c;return-1}function e(a,b){return"[object "+b+"]"===Object.prototype.toString.call(a)}function f(a){return e(a,"RegExp")}function g(a){return e(a,"Array")}function h(a){return e(a,"Function")}function i(a){var b;return a===null||a==="null"?b=null:a==="true"?b=!0:a==="false"?b=!1:a===c||a==="undefined"?b=c:a===""||isNaN(a)?b=a:b=parseFloat(a),b}function j(a){var b=a.length,c=[];while(b--)c[b]=i(a[b]);return c}function k(){this._routes=[],this._prevRoutes=[],this.bypassed=new a.Signal,this.routed=new a.Signal}function l(c,d,e,g){var h=f(c),i=b.patternLexer;this._router=g,this._pattern=c,this._paramsIds=h?null:i.getParamIds(this._pattern),this._optionalParamsIds=h?null:i.getOptionalParamsIds(this._pattern),this._matchRegexp=h?c:i.compilePattern(c),this.matched=new a.Signal,this.switched=new a.Signal,d&&this.matched.add(d),this._priority=e||0}var b,c;return k.prototype={normalizeFn:null,create:function(){return new k},shouldTypecast:!1,addRoute:function(a,b,c){var d=new l(a,b,c,this);return this._sortedInsert(d),d},removeRoute:function(a){var b=d(this._routes,a);b!==-1&&this._routes.splice(b,1),a._destroy()},removeAllRoutes:function(){var a=this.getNumRoutes();while(a--)this._routes[a]._destroy();this._routes.length=0},parse:function(a,b){a=a||"",b=b||[];var c=this._getMatchedRoutes(a),d=0,e=c.length,f;if(e){this._notifyPrevRoutes(a),this._prevRoutes=c;while(d<e)f=c[d],f.route.matched.dispatch.apply(f.route.matched,b.concat(f.params)),f.isFirst=!d,this.routed.dispatch.apply(this.routed,b.concat([a,f])),d+=1}else this.bypassed.dispatch.apply(this.bypassed,b.concat([a]))},_notifyPrevRoutes:function(a){var b=0,c;while(c=this._prevRoutes[b++])c.route.switched&&c.route.switched.dispatch(a)},getNumRoutes:function(){return this._routes.length},_sortedInsert:function(a){var b=this._routes,c=b.length;do--c;while(b[c]&&a._priority<=b[c]._priority);b.splice(c+1,0,a)},_getMatchedRoutes:function(a){var b=[],c=this._routes,d=c.length,e;while(e=c[--d])(!b.length||e.greedy)&&e.match(a)&&b.push({route:e,params:e._getParamsArray(a)});return b},toString:function(){return"[crossroads numRoutes:"+this.getNumRoutes()+"]"}},b=new k,b.VERSION="0.8.0",b.NORM_AS_ARRAY=function(a,b){return[b.vals_]},b.NORM_AS_OBJECT=function(a,b){return[b]},l.prototype={greedy:!1,rules:void 0,match:function(a){return this._matchRegexp.test(a)&&this._validateParams(a)},_validateParams:function(a){var b=this.rules,c=this._getParamsObject(a),d;for(d in b)if(d!=="normalize_"&&b.hasOwnProperty(d)&&!this._isValidParam(a,d,c))return!1;return!0},_isValidParam:function(a,b,c){var e=this.rules[b],i=c[b],j=!1;return i==null&&this._optionalParamsIds&&d(this._optionalParamsIds,b)!==-1?j=!0:f(e)?j=e.test(i):g(e)?j=d(e,i)!==-1:h(e)&&(j=e(i,a,c)),j},_getParamsObject:function(a){var c=this._router.shouldTypecast,d=b.patternLexer.getParamValues(a,this._matchRegexp,c),e={},f=d.length;while(f--)e[f]=d[f],this._paramsIds&&(e[this._paramsIds[f]]=d[f]);return e.request_=c?i(a):a,e.vals_=d,e},_getParamsArray:function(a){var c=this.rules?this.rules.normalize_:null,d;return c=c||this._router.normalizeFn,c&&h(c)?d=c(a,this._getParamsObject(a)):d=b.patternLexer.getParamValues(a,this._matchRegexp,this._router.shouldTypecast),d},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()+"]"}},b.patternLexer=function(){function w(a,b){var c=[],d;while(d=a.exec(b))c.push(d[1]);return c}function x(a){return w(g,a)}function y(a){return w(f,a)}function z(c){return c=c||"",c&&(c=c.replace(b,""),c=A(c),c=c.replace(a,"\\$&"),c=B(c)),new RegExp("^"+c+"/?$")}function A(a){return a=a.replace(c,"$1"+p+"$2"),a=a.replace(d,"$1"+o+"$2"),a=a.replace(i,n),a=a.replace(h,m),a=a.replace(f,l),a.replace(e,k)}function B(a){return a=a.replace(u,"\\/?"),a=a.replace(v,"\\/"),a=a.replace(t,"(.*)?"),a=a.replace(s,"(.+)"),a=a.replace(r,"([^\\/]+)?/?"),a.replace(q,"([^\\/]+)")}function C(a,b,c){var d=b.exec(a);return d&&(d.shift(),c&&(d=j(d))),d}var a=/[\\.+*?\^$\[\](){}\/'#]/g,b=/\/$/g,c=/([:}]|\w(?=\/))\/?(:)/g,d=/([:}])\/?(\{)/g,e=/\{([^}]+)\}/g,f=/:([^:]+):/g,g=/(?:\{|:)([^}:]+)(?:\}|:)/g,h=/\{([^}]+)\*\}/g,i=/:([^:]+)\*:/g,k="__CR_RP__",l="__CR_OP__",m="__CR_RR__",n="__CR_OR__",o="__CR_RS__",p="__CR_OS__",q=new RegExp(k,"g"),r=new RegExp(l,"g"),s=new RegExp(m,"g"),t=new RegExp(n,"g"),u=new RegExp(p,"g"),v=new RegExp(o,"g");return{getParamIds:x,getOptionalParamsIds:y,getParamValues:C,compilePattern:z}}(),b})})(typeof define=="function"&&define.amd?define:function(a,b){typeof module!="undefined"&&module.exports?module.exports=b(require(a[0])):window.crossroads=b(window[a[0]])})
+(function(a){a(["signals"],function(a){function d(a,b){if(a.indexOf)return a.indexOf(b);var c=a.length;while(c--)if(a[c]===b)return c;return-1}function e(a,b){return"[object "+b+"]"===Object.prototype.toString.call(a)}function f(a){return e(a,"RegExp")}function g(a){return e(a,"Array")}function h(a){return typeof a=="function"}function i(a){var b;return a===null||a==="null"?b=null:a==="true"?b=!0:a==="false"?b=!1:a===c||a==="undefined"?b=c:a===""||isNaN(a)?b=a:b=parseFloat(a),b}function j(a){var b=a.length,c=[];while(b--)c[b]=i(a[b]);return c}function k(a){var b=(a||"").replace("?","").split("&"),c=b.length,d={},e,f;while(c--)e=b[c].split("="),f=i(e[1]),d[e[0]]=typeof f=="string"?decodeURIComponent(f):f;return d}function l(){this._routes=[],this._prevRoutes=[],this.bypassed=new a.Signal,this.routed=new a.Signal}function m(c,d,e,g){var h=f(c),i=b.patternLexer;this._router=g,this._pattern=c,this._paramsIds=h?null:i.getParamIds(this._pattern),this._optionalParamsIds=h?null:i.getOptionalParamsIds(this._pattern),this._matchRegexp=h?c:i.compilePattern(c),this.matched=new a.Signal,this.switched=new a.Signal,d&&this.matched.add(d),this._priority=e||0}var b,c;return l.prototype={greedy:!1,greedyEnabled:!0,normalizeFn:null,create:function(){return new l},shouldTypecast:!1,addRoute:function(a,b,c){var d=new m(a,b,c,this);return this._sortedInsert(d),d},removeRoute:function(a){var b=d(this._routes,a);b!==-1&&this._routes.splice(b,1),a._destroy()},removeAllRoutes:function(){var a=this.getNumRoutes();while(a--)this._routes[a]._destroy();this._routes.length=0},parse:function(a,b){a=a||"",b=b||[];var c=this._getMatchedRoutes(a),d=0,e=c.length,f;if(e){this._notifyPrevRoutes(c,a),this._prevRoutes=c;while(d<e)f=c[d],f.route.matched.dispatch.apply(f.route.matched,b.concat(f.params)),f.isFirst=!d,this.routed.dispatch.apply(this.routed,b.concat([a,f])),d+=1}else this.bypassed.dispatch.apply(this.bypassed,b.concat([a]))},_notifyPrevRoutes:function(a,b){var c=0,d;while(d=this._prevRoutes[c++])d.route.switched&&this._didSwitch(d.route,a)&&d.route.switched.dispatch(b)},_didSwitch:function(a,b){var c,d=0;while(c=b[d++])if(c.route===a)return!1;return!0},getNumRoutes:function(){return this._routes.length},_sortedInsert:function(a){var b=this._routes,c=b.length;do--c;while(b[c]&&a._priority<=b[c]._priority);b.splice(c+1,0,a)},_getMatchedRoutes:function(a){var b=[],c=this._routes,d=c.length,e;while(e=c[--d]){(!b.length||this.greedy||e.greedy)&&e.match(a)&&b.push({route:e,params:e._getParamsArray(a)});if(!this.greedyEnabled&&b.length)break}return b},toString:function(){return"[crossroads numRoutes:"+this.getNumRoutes()+"]"}},b=new l,b.VERSION="0.9.0-alpha",b.NORM_AS_ARRAY=function(a,b){return[b.vals_]},b.NORM_AS_OBJECT=function(a,b){return[b]},m.prototype={greedy:!1,rules:void 0,match:function(a){return a=a||"",this._matchRegexp.test(a)&&this._validateParams(a)},_validateParams:function(a){var b=this.rules,c=this._getParamsObject(a),d;for(d in b)if(d!=="normalize_"&&b.hasOwnProperty(d)&&!this._isValidParam(a,d,c))return!1;return!0},_isValidParam:function(a,b,c){var e=this.rules[b],i=c[b],j=!1,k=b.indexOf("?")===0;return i==null&&this._optionalParamsIds&&d(this._optionalParamsIds,b)!==-1?j=!0:f(e)?(k&&(i=c[b+"_"]),j=e.test(i)):g(e)?(k&&(i=c[b+"_"]),j=d(e,i)!==-1):h(e)&&(j=e(i,a,c)),j},_getParamsObject:function(a){var c=this._router.shouldTypecast,d=b.patternLexer.getParamValues(a,this._matchRegexp,c),e={},f=d.length,g,h;while(f--)h=d[f],this._paramsIds&&(g=this._paramsIds[f],g.indexOf("?")===0&&h&&(e[g+"_"]=h,h=k(h),d[f]=h),e[g]=h),e[f]=h;return e.request_=c?i(a):a,e.vals_=d,e},_getParamsArray:function(a){var b=this.rules?this.rules.normalize_:null,c;return b=b||this._router.normalizeFn,b&&h(b)?c=b(a,this._getParamsObject(a)):c=this._getParamsObject(a).vals_,c},interpolate:function(a){var c=b.patternLexer.interpolate(this._pattern,a);if(!this._validateParams(c))throw new Error("Generated string doesn't validate against `Route.rules`.");return c},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()+"]"}},b.patternLexer=function(){function k(){var a,b;for(a in e)e.hasOwnProperty(a)&&(b=e[a],b.id="__CR_"+a+"__",b.save="save"in b?b.save.replace("{{id}}",b.id):b.id,b.rRestore=new RegExp(b.id,"g"))}function l(a,b){var c=[],d;while(d=a.exec(b))c.push(d[1]);return c}function m(a){return l(d,a)}function n(a){return l(e.OP.rgx,a)}function o(d){return d=d||"",d&&(i===f?d=d.replace(b,""):i===h&&(d=d.replace(c,"")),d=p(d,"rgx","save"),d=d.replace(a,"\\$&"),d=p(d,"rRestore","res"),i===f&&(d="\\/?"+d)),i!==g&&(d+="\\/?"),new RegExp("^"+d+"$")}function p(a,b,c){var d,f;for(f in e)e.hasOwnProperty(f)&&(d=e[f],a=a.replace(d[b],d[c]));return a}function q(a,b,c){var d=b.exec(a);return d&&(d.shift(),c&&(d=j(d))),d}function r(a,b){if(typeof a!="string")throw new Error("Route pattern should be a string.");var c=function(a,c){var d;if(c in b){d=b[c];if(a.indexOf("*")===-1&&d.indexOf("/")!==-1)throw new Error('Invalid value "'+d+'" for segment "'+a+'".')}else{if(a.indexOf("{")!==-1)throw new Error("The segment "+a+" is required.");d=""}return d};return e.OS.trail||(e.OS.trail=new RegExp("(?:"+e.OS.id+")+$")),a.replace(e.OS.rgx,e.OS.save).replace(d,c).replace(e.OS.trail,"").replace(e.OS.rRestore,"/")}var a=/[\\.+*?\^$\[\](){}\/'#]/g,b=/^\/|\/$/g,c=/\/$/g,d=/(?:\{|:)([^}:]+)(?:\}|:)/g,e={OS:{rgx:/([:}]|\w(?=\/))\/?(:|(?:\{\?))/g,save:"$1{{id}}$2",res:"\\/?"},RS:{rgx:/([:}])\/?(\{)/g,save:"$1{{id}}$2",res:"\\/"},RQ:{rgx:/\{\?([^}]+)\}/g,res:"\\?([^#]+)"},OQ:{rgx:/:\?([^:]+):/g,res:"(?:\\?([^#]*))?"},OR:{rgx:/:([^:]+)\*:/g,res:"(.*)?"},RR:{rgx:/\{([^}]+)\*\}/g,res:"(.+)"},RP:{rgx:/\{([^}]+)\}/g,res:"([^\\/?]+)"},OP:{rgx:/:([^:]+):/g,res:"([^\\/?]+)?/?"}},f=1,g=2,h=3,i=f;return k(),{strict:function(){i=g},loose:function(){i=f},legacy:function(){i=h},getParamIds:m,getOptionalParamsIds:n,getParamValues:q,compilePattern:o,interpolate:r}}(),b})})(typeof define=="function"&&define.amd?define:function(a,b){typeof module!="undefined"&&module.exports?module.exports=b(require(a[0])):window.crossroads=b(window[a[0]])})
View
2 package.json
@@ -3,7 +3,7 @@
"description" : "Flexible router which can be used in multiple environments",
"keywords" : ["routes", "event", "observer", "routing", "router"],
"homepage" : "http://millermedeiros.github.com/crossroads.js/",
- "version" : "0.8.0",
+ "version" : "0.9.0-alpha",
"author" : {
"name" : "Miller Medeiros",
"url" : "http://blog.millermedeiros.com/"

0 comments on commit 651085d

Please sign in to comment.
Something went wrong with that request. Please try again.