diff --git a/README.md b/README.md index c102a31..df022cb 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,20 @@ console.log(route.match('/products')); // => {} console.log(route.match('/products/321')); // => { id : '321' } ``` +* With multiple patterns: + +```javascript +var route = Susanin.Route({ + pattern: [ + { name: 'products', test: '/products(/)' }, + { name: 'productsWithSize', test: '/products//' } + ] +})); + +console.log(route.match('/products/321')); // => { id: '321' } +console.log(route.match('/products/321/xxl')); // => { id: '321', size: 'xxl' } +``` + * With a default value of param: ```javascript diff --git a/dist/susanin.js b/dist/susanin.js index 8ca08e2..a4d1b63 100644 --- a/dist/susanin.js +++ b/dist/susanin.js @@ -217,6 +217,10 @@ var PARAM_NAME_REGEXP_SOURCE = '[a-zA-Z_][\\w\\-]*'; */ var PARAM_VALUE_REGEXP_SOURCE = '[\\w\\-\\.~]+'; +var OPTIONAL_PARAM_WEIGHT = 1; +var REQUIRED_PARAM_WEIGHT = 2; +var MINIMAL_PATTERN_WEIGHT = 1; + /** * @const * @type {RegExp} @@ -250,6 +254,12 @@ var TRAILING_SLASH_PARAM_VALUE = '/'; */ var TRAILING_SLASH_PARAM_VALUE_ESCAPED = escape('/'); +/** + * @const + * @type {String} + */ +var DEFAULT_PATTERN_NAME = 'pattern_' + EXPANDO; + /** * @const * @type {String} @@ -279,48 +289,94 @@ function Route(options) { return new Route(options); } - typeof options === 'string' && (options = { pattern : options }); + this._setOptions(options); +} - if ( ! options || typeof options !== 'object') { - throw new Error('You must specify options'); +Route.prototype._setOptions = function(opts) { + var options = opts; + + if (typeof options === 'string') { + options = { pattern : options }; } - if (typeof options.pattern !== 'string') { - throw new Error('You must specify the pattern of the route'); + if (! options || typeof options !== 'object') { + throw new Error('You must specify options'); } - /** - * @type {RouteOptions} - * @private - */ this._options = options; - this._conditions = options.conditions && typeof options.conditions === 'object' ? options.conditions : {}; + this._setConditions(this._options.conditions); + this._setPatterns(this._options.pattern); +}; + +Route.prototype._setConditions = function(conditions) { + var opts = this._options; - if (options.isTrailingSlashOptional !== false) { - options.pattern += GROUP_OPENED_CHAR + PARAM_OPENED_CHAR + - TRAILING_SLASH_PARAM_NAME + - PARAM_CLOSED_CHAR + GROUP_CLOSED_CHAR; + this._conditions = conditions && typeof conditions === 'object' ? conditions : {}; + + if (opts.isTrailingSlashOptional !== false) { this._conditions[TRAILING_SLASH_PARAM_NAME] = TRAILING_SLASH_PARAM_VALUE_ESCAPED; } - options.pattern += GROUP_OPENED_CHAR + - '?' + PARAM_OPENED_CHAR + QUERY_STRING_PARAM_NAME + PARAM_CLOSED_CHAR + - GROUP_CLOSED_CHAR; this._conditions[QUERY_STRING_PARAM_NAME] = '.*'; +}; - this._paramsMap = []; - this._mainParamsMap = {}; - this._requiredParams = []; +Route.prototype._setPatterns = function(patternOpts) { + var opts = this._options, + defaultPattern, + patterns, + patternIndex, + patternsSize, + currentPattern, + patternName, + patternTest, + parsedPattern, + pattern; + + defaultPattern = { + name : DEFAULT_PATTERN_NAME, + test : patternOpts + }; + patterns = isArray(patternOpts) ? patternOpts : [ defaultPattern ]; - /** - * @type {Array} - * @private - */ - this._parts = this._parsePattern(options.pattern); + this._patterns = {}; + this._patternNames = []; - this._buildParseRegExp(); - this._buildBuildFn(); -} + for (patternIndex = 0, patternsSize = patterns.length; patternIndex < patternsSize; patternIndex++) { + currentPattern = patterns[patternIndex]; + patternName = currentPattern.name; + patternTest = currentPattern.test; + + if (typeof patternTest !== 'string' || typeof patternName !== 'string') { + throw new Error('You must specify the pattern of the route'); + } + + if (opts.isTrailingSlashOptional !== false) { + patternTest += GROUP_OPENED_CHAR + PARAM_OPENED_CHAR + + TRAILING_SLASH_PARAM_NAME + + PARAM_CLOSED_CHAR + GROUP_CLOSED_CHAR; + } + + patternTest += GROUP_OPENED_CHAR + + '?' + PARAM_OPENED_CHAR + QUERY_STRING_PARAM_NAME + PARAM_CLOSED_CHAR + + GROUP_CLOSED_CHAR; + + parsedPattern = this._parsePattern(patternTest); + pattern = { + name : patternName, + test : patternTest, + params : parsedPattern.params, + mainParams : parsedPattern.mainParams, + requiredParams : parsedPattern.requiredParams, + optionalParams : parsedPattern.optionalParams, + parts : parsedPattern.parts + }; + + this._buildParseRegExp(pattern); + this._buildBuildFn(pattern); + this._patterns[patternName] = pattern; + this._patternNames.push(patternName); + } +}; /** * @param {String} pattern @@ -329,14 +385,54 @@ function Route(options) { * @private */ Route.prototype._parsePattern = function(pattern, isOptional) { - var parts = [], - part = '', + var part = '', character, i = 0, j, size, countOpened = 0, isFindingClosed = false, length = pattern.length, - what; + what, + parsedPattern, + parseResult; + + parseResult = { + parts : [], + params : [], + mainParams : [], + requiredParams : [], + optionalParams : [] + }; + + var addParams = function(params, requiredParams) { + var paramIndex, + paramsSize, + param; + + for (paramIndex = 0, paramsSize = params.length; paramIndex < paramsSize; paramIndex++) { + param = params[paramIndex]; + + parseResult.params.push(param); + parseResult.mainParams[param] = true; + + if (requiredParams.indexOf(param) !== -1) { + parseResult.requiredParams.push(param); + } else { + parseResult.optionalParams.push(param); + } + } + }; + + var addParts = function(parts) { + var partIndex, + partsSize, + part; + + for (partIndex = 0, partsSize = parts.length; partIndex < partsSize; partIndex++) { + part = parts[partIndex]; + + parseResult.parts.push(part); + } + }; while (i < length) { character = pattern.charAt(i++); @@ -346,7 +442,10 @@ Route.prototype._parsePattern = function(pattern, isOptional) { ++countOpened; part += character; } else { - this._parseParams(part, parts, isOptional); + parsedPattern = this._parseParams(part, isOptional); + addParams(parsedPattern.params, parsedPattern.requiredParams); + addParts(parsedPattern.parts); + part = ''; countOpened = 0; isFindingClosed = true; @@ -354,13 +453,16 @@ Route.prototype._parsePattern = function(pattern, isOptional) { } else if (character === GROUP_CLOSED_CHAR) { if (isFindingClosed) { if (countOpened === 0) { + parsedPattern = this._parsePattern(part, true); + part = { what : 'optional', dependOnParams : [], - parts : this._parsePattern(part, true) + parts : parsedPattern.parts }; - parts.push(part); + addParams(parsedPattern.params, parsedPattern.requiredParams); + addParts([ part ]); for (j = 0, size = part.parts.length; j < size; ++j) { what = part.parts[j] && part.parts[j].what; @@ -386,22 +488,30 @@ Route.prototype._parsePattern = function(pattern, isOptional) { } } - this._parseParams(part, parts, isOptional); + parsedPattern = this._parseParams(part, isOptional); + addParams(parsedPattern.params, parsedPattern.requiredParams); + addParts(parsedPattern.parts); - return parts; + return parseResult; }; /** * @param {String} pattern - * @param {Array} parts * @param {Boolean} isOptional * @private */ -Route.prototype._parseParams = function(pattern, parts, isOptional) { +Route.prototype._parseParams = function(pattern, isOptional) { var matches = pattern.match(PARSE_PARAMS_REGEXP), i, size, part, - paramName; + paramName, + parseResult; + + parseResult = { + parts : [], + params : [], + requiredParams : [] + }; if (matches) { for (i = 0, size = matches.length; i < size; ++i) { @@ -409,27 +519,28 @@ Route.prototype._parseParams = function(pattern, parts, isOptional) { if (part.charAt(0) === PARAM_OPENED_CHAR && part.charAt(part.length - 1) === PARAM_CLOSED_CHAR) { paramName = part.substr(1, part.length - 2); - this._paramsMap.push(paramName); - this._mainParamsMap[paramName] = true; - isOptional || this._requiredParams.push(paramName); - parts.push({ + parseResult.params.push(paramName); + parseResult.parts.push({ what : 'param', name : paramName }); + isOptional || parseResult.requiredParams.push(paramName); } else { - parts.push(part); + parseResult.parts.push(part); } } } + + return parseResult; }; /** * @private */ -Route.prototype._buildParseRegExp = function() { - this._parseRegExpSource = '^' + this._buildParseRegExpParts(this._parts) + '$'; - this._parseRegExp = new RegExp(this._parseRegExpSource); +Route.prototype._buildParseRegExp = function(pattern) { + pattern.parseRegExpSource = '^' + this._buildParseRegExpParts(pattern.parts) + '$'; + pattern.parseRegExp = new RegExp(pattern.parseRegExpSource); }; /** @@ -516,10 +627,10 @@ Route.prototype._checkParamValue = function(paramName, paramValue) { /** * @private */ -Route.prototype._buildBuildFn = function() { - this._buildFnSource = 'var h=({}).hasOwnProperty;return ' + this._buildBuildFnParts(this._parts) + ';'; +Route.prototype._buildBuildFn = function(pattern) { + pattern.buildFnSource = 'var h=({}).hasOwnProperty;return ' + this._buildBuildFnParts(pattern.parts) + ';'; /*jshint evil:true */ - this._buildFn = new Function('p', this._buildFnSource); + pattern.buildFn = new Function('p', pattern.buildFnSource); }; /** @@ -539,7 +650,6 @@ Route.prototype._buildBuildFnParts = function(parts) { if (typeof part === 'string') { ret += '+"' + escape(part) + '"' ; } else if (part.what === 'param') { - this._mainParamsMap[part.name] = true; ret += '+(h.call(p,"' + escape(part.name) + '")?' + 'p["' + escape(part.name) + '"]:' + (defaults && has(defaults, part.name) ? @@ -607,20 +717,34 @@ Route.prototype.match = function(path, data) { queryString, options = this._options, filter = options.postMatch, - defaults = options.defaults; + defaults = options.defaults, + patterns = this._patterns, + currentPattern, + pattern, + patternName; if (typeof path !== 'string' || (data && ! this._isDataMatched(data))) { return ret; } - matches = path.match(this._parseRegExp); + for (patternName in patterns) { + if (has(patterns, patternName)) { + pattern = patterns[patternName]; + matches = path.match(pattern.parseRegExp); + + if (matches) { + currentPattern = pattern; + break; + } + } + } if (matches) { ret = {}; for (i = 1, size = matches.length; i < size; ++i) { if (typeof matches[i] !== 'undefined' && /* for IE lt 9*/ matches[i] !== '') { - paramName = this._paramsMap[i - 1]; + paramName = currentPattern.params[i - 1]; if (paramName === QUERY_STRING_PARAM_NAME) { queryString = matches[i]; } else if (paramName === TRAILING_SLASH_PARAM_NAME) { @@ -638,7 +762,7 @@ Route.prototype.match = function(path, data) { for (paramName in queryParams) { if (has(queryParams, paramName) && ! has(ret, paramName)) { paramValue = queryParams[paramName]; - if (this._mainParamsMap[paramName] && isArray(paramValue)) { + if (currentPattern.mainParams[paramName] && isArray(paramValue)) { paramValue = paramValue[0]; } @@ -673,6 +797,74 @@ Route.prototype.match = function(path, data) { return ret; }; +Route.prototype._getPatternParamsIntersection = function(patternParams, params) { + var intersection = [], + paramIndex, + size, + paramName; + + for (paramIndex = 0, size = patternParams.length; paramIndex < size; paramIndex++) { + paramName = patternParams[paramIndex]; + + if (has(params, paramName)) { + intersection.push(paramName); + } + } + + return intersection; +}; + +Route.prototype._getPatternForBuild = function(params, isStrict) { + var patterns = this._patterns, + maxPatternWeight = 0, + patternWeight, + patternName, + pattern, + patternParams, + requiredParams, + requiredParamsCount, + optionalParams, + paramsIntersection, + patternForBuild = null; + + for (patternName in patterns) { + if (has(patterns, patternName)) { + pattern = patterns[patternName]; + + if (isStrict) { + requiredParams = pattern.requiredParams; + requiredParamsCount = requiredParams.length; + paramsIntersection = this._getPatternParamsIntersection(requiredParams, params); + + if (requiredParamsCount !== paramsIntersection.length) { + continue; + } + + optionalParams = pattern.optionalParams; + paramsIntersection = this._getPatternParamsIntersection(optionalParams, params); + patternWeight = paramsIntersection.length * OPTIONAL_PARAM_WEIGHT + + requiredParamsCount * REQUIRED_PARAM_WEIGHT; + } else { + patternParams = pattern.params; + paramsIntersection = this._getPatternParamsIntersection(patternParams, params); + + patternWeight = paramsIntersection.length * REQUIRED_PARAM_WEIGHT; + } + + if (! patternWeight) { + patternWeight = MINIMAL_PATTERN_WEIGHT; + } + + if (patternWeight > maxPatternWeight) { + maxPatternWeight = patternWeight; + patternForBuild = pattern; + } + } + } + + return patternForBuild; +}; + /** * Build path from params * @param {Object} params @@ -685,36 +877,37 @@ Route.prototype.build = function(params, isStrict) { useQueryString = options.useQueryString !== false, queryParams = {}, queryString, + pattern, paramName, paramValue, filter = options.preBuild, - i, size; + buildParams; + + buildParams = params || {}; if (typeof filter === 'function') { - params = filter(params); + buildParams = filter(buildParams); + } + + pattern = this._getPatternForBuild(buildParams, isStrict); + + if (! pattern) { + return null; } - for (paramName in params) { + for (paramName in buildParams) { if ( - has(params, paramName) && - params[paramName] !== null && - typeof params[paramName] !== 'undefined' && - (this._mainParamsMap[paramName] || useQueryString) + has(buildParams, paramName) && + buildParams[paramName] !== null && + typeof buildParams[paramName] !== 'undefined' && + (pattern.mainParams[paramName] || useQueryString) ) { - paramValue = params[paramName]; + paramValue = buildParams[paramName]; if (isStrict && ! this._checkParamValue(paramName, paramValue)) { return null; } - (this._mainParamsMap[paramName] ? newParams : queryParams)[paramName] = paramValue; - } - } - - if (isStrict) { - for (i = 0, size = this._requiredParams.length; i < size; ++i) { - if ( ! has(newParams, this._requiredParams[i])) { - return null; - } + (pattern.mainParams[paramName] ? newParams : queryParams)[paramName] = paramValue; } } @@ -723,7 +916,7 @@ Route.prototype.build = function(params, isStrict) { queryString && (newParams[QUERY_STRING_PARAM_NAME] = queryString); } - return this._buildFn(newParams); + return pattern.buildFn(newParams); }; /** diff --git a/dist/susanin.min.js b/dist/susanin.min.js index 64058f6..50e2a15 100644 --- a/dist/susanin.min.js +++ b/dist/susanin.min.js @@ -1 +1 @@ -!function(t){function e(t){return{"./querystring":function(){var t={},e=Object.prototype.hasOwnProperty,r=Object.prototype.toString,n=function(t){return"[object Array]"===r.call(t)},i={decode:function(t){var e;try{e=decodeURIComponent(t.replace(/\+/g,"%20"))}catch(r){e=t}return e},parse:function(t,r,o){var a,s,p,u,f,h,c,l={};if("string"!=typeof t||""===t)return l;for(r||(r="&"),o||(o="="),a=t.split(r),u=0,f=a.length;f>u;++u)h=i.decode(a[u]),c=h.indexOf(o),c>=0?(p=h.substr(0,c),s=h.substr(c+1)):(p=h,s=""),e.call(l,p)?n(l[p])?l[p].push(s):l[p]=[l[p],s]:l[p]=s;return l},stringify:function(t,r,n){var i,o,a,s,p,u,f="";if(!t)return f;r||(r="&"),n||(n="=");for(u in t)if(e.call(t,u))for(a=[].concat(t[u]),s=0,p=a.length;p>s;++s)o=typeof a[s],i="object"===o||"undefined"===o?"":encodeURIComponent(a[s]),f+=r+encodeURIComponent(u)+n+i;return f.substr(r.length)}};return t.exports=i,t.exports},"./route":function(){function t(e){if(!(this instanceof t))return new t(e);if("string"==typeof e&&(e={pattern:e}),!e||"object"!=typeof e)throw new Error("You must specify options");if("string"!=typeof e.pattern)throw new Error("You must specify the pattern of the route");this._options=e,this._conditions=e.conditions&&"object"==typeof e.conditions?e.conditions:{},e.isTrailingSlashOptional!==!1&&(e.pattern+=c+f+g+h+l,this._conditions[g]=P),e.pattern+=c+"?"+f+b+h+l,this._conditions[b]=".*",this._paramsMap=[],this._mainParamsMap={},this._requiredParams=[],this._parts=this._parsePattern(e.pattern),this._buildParseRegExp(),this._buildBuildFn()}var r={},n=Object.prototype.hasOwnProperty,i=function(t,e){return n.call(t,e)},o=Object.prototype.toString,a=function(t){return"[object Array]"===o.call(t)},s=e("./querystring"),p=function(){var t=["/",".","*","+","?","|","(",")","[","]","{","}","\\"],e=new RegExp("(\\"+t.join("|\\")+")","g");return function(t){return t.replace(e,"\\$1")}}(),u=String(Math.random()).substr(2,5),f="<",h=">",c="(",l=")",d="[a-zA-Z_][\\w\\-]*",m="[\\w\\-\\.~]+",_=new RegExp("("+p(f)+d+p(h)+"|"+"[^"+p(f)+p(h)+"]+"+"|"+p(f)+"|"+p(h)+")","g"),g="ts_"+u,y="/",P=p("/"),b="qs_"+u;return t.prototype._parsePattern=function(t,e){for(var r,n,i,o,a=[],s="",p=0,u=0,f=!1,h=t.length;h>p;)if(r=t.charAt(p++),r===c)f?(++u,s+=r):(this._parseParams(s,a,e),s="",u=0,f=!0);else if(r===l)if(f)if(0===u){for(s={what:"optional",dependOnParams:[],parts:this._parsePattern(s,!0)},a.push(s),n=0,i=s.parts.length;i>n;++n)o=s.parts[n]&&s.parts[n].what,"param"===o?s.dependOnParams.push(s.parts[n].name):"optional"===o&&s.dependOnParams.push.apply(s.dependOnParams,s.parts[n].dependOnParams);s="",f=!1}else--u,s+=r;else s+=r;else s+=r;return this._parseParams(s,a,e),a},t.prototype._parseParams=function(t,e,r){var n,i,o,a,s=t.match(_);if(s)for(n=0,i=s.length;i>n;++n)o=s[n],o.charAt(0)===f&&o.charAt(o.length-1)===h?(a=o.substr(1,o.length-2),this._paramsMap.push(a),this._mainParamsMap[a]=!0,r||this._requiredParams.push(a),e.push({what:"param",name:a})):e.push(o)},t.prototype._buildParseRegExp=function(){this._parseRegExpSource="^"+this._buildParseRegExpParts(this._parts)+"$",this._parseRegExp=new RegExp(this._parseRegExpSource)},t.prototype._buildParseRegExpParts=function(t){var e,r,n,i="";for(e=0,r=t.length;r>e;++e)n=t[e],i+="string"==typeof n?p(n):"param"===n.what?"("+(this._getParamValueRegExpSource(n.name)||m)+")":"(?:"+this._buildParseRegExpParts(n.parts)+")?";return i},t.prototype._getParamValueRegExpSource=function(t){var e,r,n=this._conditionRegExpSources||(this._conditionRegExpSources={}),o=this._conditions;return i(n,t)||(i(o,t)?(r=o[t],e=a(r)?"(?:"+r.join("|")+")":r+""):e=null,n[t]=e),n[t]},t.prototype._getParamValueRegExp=function(t){var e,r=this._conditionRegExps||(this._conditionRegExps={});return i(r,t)||(e=this._getParamValueRegExpSource(t),r[t]=e?new RegExp("^"+e+"$"):null),r[t]},t.prototype._checkParamValue=function(t,e){var r=this._getParamValueRegExp(t);return r?r.test(e):!0},t.prototype._buildBuildFn=function(){this._buildFnSource="var h=({}).hasOwnProperty;return "+this._buildBuildFnParts(this._parts)+";",this._buildFn=new Function("p",this._buildFnSource)},t.prototype._buildBuildFnParts=function(t){var e,r,n,o,a,s,u='""',f=this._options.defaults;for(e=0,r=t.length;r>e;++e)if(a=t[e],"string"==typeof a)u+='+"'+p(a)+'"';else if("param"===a.what)this._mainParamsMap[a.name]=!0,u+='+(h.call(p,"'+p(a.name)+'")?'+'p["'+p(a.name)+'"]:'+(f&&i(f,a.name)?'"'+p(f[a.name])+'"':'""')+")";else{for(u+="+((false",n=0,o=a.dependOnParams.length;o>n;++n)s=a.dependOnParams[n],u+='||(h.call(p,"'+p(s)+'")'+(f&&i(f,s)?'&&p["'+p(s)+'"]!=="'+p(f[s])+'"':"")+")";u+=")?("+this._buildBuildFnParts(a.parts)+'):"")'}return u},t.prototype._isDataMatched=function(t){var e,r=this._options.data;if("function"==typeof t)return Boolean(t(r));if(t&&"object"==typeof t)for(e in t)if(i(t,e)&&(!r||"object"!=typeof r||r[e]!==t[e]))return!1;return!0},t.prototype.match=function(t,e){var r,n,o,p,u,f,h,c=null,l=this._options,d=l.postMatch,m=l.defaults;if("string"!=typeof t||e&&!this._isDataMatched(e))return c;if(r=t.match(this._parseRegExp)){for(c={},n=1,o=r.length;o>n;++n)if("undefined"!=typeof r[n]&&""!==r[n])if(p=this._paramsMap[n-1],p===b)h=r[n];else if(p===g){if(t.charAt(t.length-2)===y)return null}else c[p]=r[n];if(h&&l.useQueryString!==!1){f=s.parse(h);for(p in f)if(i(f,p)&&!i(c,p))if(u=f[p],this._mainParamsMap[p]&&a(u)&&(u=u[0]),a(u))for(c[p]=[],n=0,o=u.length;o>n;++n)this._checkParamValue(p,u[n])&&c[p].push(u[n]);else this._checkParamValue(p,u)&&(c[p]=u)}for(p in m)i(m,p)&&!i(c,p)&&(c[p]=m[p])}return c&&"function"==typeof d&&(c=d(c),c&&"object"==typeof c||(c=null)),c},t.prototype.build=function(t,e){var r,n,o,a,p,u=this._options,f={},h=u.useQueryString!==!1,c={},l=u.preBuild;"function"==typeof l&&(t=l(t));for(n in t)if(i(t,n)&&null!==t[n]&&"undefined"!=typeof t[n]&&(this._mainParamsMap[n]||h)){if(o=t[n],e&&!this._checkParamValue(n,o))return null;(this._mainParamsMap[n]?f:c)[n]=o}if(e)for(a=0,p=this._requiredParams.length;p>a;++a)if(!i(f,this._requiredParams[a]))return null;return h&&(r=s.stringify(c),r&&(f[b]=r)),this._buildFn(f)},t.prototype.getData=function(){return this._options.data},t.prototype.getName=function(){return this._options.name},r.exports=t,r.exports},"./router":function(){function t(){return this instanceof t?(this._routes=[],this._routesByName={},void 0):new t}var r={},n=e("./route");return t.prototype.addRoute=function(t){var e,r;return e=new n(t),this._routes.push(e),r=e.getName(),r&&(this._routesByName[r]=e),e},t.prototype.find=function(){var t,e,r,n=[],i=this._routes;for(e=0,r=i.length;r>e;++e)t=i[e].match.apply(i[e],arguments),null!==t&&n.push([i[e],t]);return n},t.prototype.findFirst=function(){var t,e,r,n=this._routes;for(e=0,r=n.length;r>e;++e)if(t=n[e].match.apply(n[e],arguments),null!==t)return[n[e],t];return null},t.prototype.getRouteByName=function(t){return this._routesByName[t]||null},t.Route=n,r.exports=t,r.exports}}[t]()}var r=e("./router"),n=!0;"object"==typeof exports&&"string"!=typeof exports.nodeName&&(module.exports=r,n=!1),t.modules&&modules.define&&modules.require&&(modules.define("susanin",function(t){t(r)}),n=!1),"function"==typeof t.define&&define.amd&&(define(function(){return r}),n=!1),n&&(t.Susanin=r)}(this); \ No newline at end of file +!function(t){function e(t){return{"./querystring":function(){var t={},e=Object.prototype.hasOwnProperty,r=Object.prototype.toString,n=function(t){return"[object Array]"===r.call(t)},a={decode:function(t){var e;try{e=decodeURIComponent(t.replace(/\+/g,"%20"))}catch(r){e=t}return e},parse:function(t,r,o){var s,i,p,u,f,h,l,c={};if("string"!=typeof t||""===t)return c;for(r||(r="&"),o||(o="="),s=t.split(r),u=0,f=s.length;f>u;++u)h=a.decode(s[u]),l=h.indexOf(o),l>=0?(p=h.substr(0,l),i=h.substr(l+1)):(p=h,i=""),e.call(c,p)?n(c[p])?c[p].push(i):c[p]=[c[p],i]:c[p]=i;return c},stringify:function(t,r,n){var a,o,s,i,p,u,f="";if(!t)return f;r||(r="&"),n||(n="=");for(u in t)if(e.call(t,u))for(s=[].concat(t[u]),i=0,p=s.length;p>i;++i)o=typeof s[i],a="object"===o||"undefined"===o?"":encodeURIComponent(s[i]),f+=r+encodeURIComponent(u)+n+a;return f.substr(r.length)}};return t.exports=a,t.exports},"./route":function(){function t(e){return this instanceof t?(this._setOptions(e),void 0):new t(e)}var r={},n=Object.prototype.hasOwnProperty,a=function(t,e){return n.call(t,e)},o=Object.prototype.toString,s=function(t){return"[object Array]"===o.call(t)},i=e("./querystring"),p=function(){var t=["/",".","*","+","?","|","(",")","[","]","{","}","\\"],e=new RegExp("(\\"+t.join("|\\")+")","g");return function(t){return t.replace(e,"\\$1")}}(),u=String(Math.random()).substr(2,5),f="<",h=">",l="(",c=")",m="[a-zA-Z_][\\w\\-]*",d="[\\w\\-\\.~]+",g=1,_=2,P=1,y=new RegExp("("+p(f)+m+p(h)+"|"+"[^"+p(f)+p(h)+"]+"+"|"+p(f)+"|"+p(h)+")","g"),b="ts_"+u,x="/",v=p("/"),R="pattern_"+u,E="qs_"+u;return t.prototype._setOptions=function(t){var e=t;if("string"==typeof e&&(e={pattern:e}),!e||"object"!=typeof e)throw new Error("You must specify options");this._options=e,this._setConditions(this._options.conditions),this._setPatterns(this._options.pattern)},t.prototype._setConditions=function(t){var e=this._options;this._conditions=t&&"object"==typeof t?t:{},e.isTrailingSlashOptional!==!1&&(this._conditions[b]=v),this._conditions[E]=".*"},t.prototype._setPatterns=function(t){var e,r,n,a,o,i,p,u,m,d=this._options;for(e={name:R,test:t},r=s(t)?t:[e],this._patterns={},this._patternNames=[],n=0,a=r.length;a>n;n++){if(o=r[n],i=o.name,p=o.test,"string"!=typeof p||"string"!=typeof i)throw new Error("You must specify the pattern of the route");d.isTrailingSlashOptional!==!1&&(p+=l+f+b+h+c),p+=l+"?"+f+E+h+c,u=this._parsePattern(p),m={name:i,test:p,params:u.params,mainParams:u.mainParams,requiredParams:u.requiredParams,optionalParams:u.optionalParams,parts:u.parts},this._buildParseRegExp(m),this._buildBuildFn(m),this._patterns[i]=m,this._patternNames.push(i)}},t.prototype._parsePattern=function(t,e){var r,n,a,o,s,i,p="",u=0,f=0,h=!1,m=t.length;i={parts:[],params:[],mainParams:[],requiredParams:[],optionalParams:[]};for(var d=function(t,e){var r,n,a;for(r=0,n=t.length;n>r;r++)a=t[r],i.params.push(a),i.mainParams[a]=!0,-1!==e.indexOf(a)?i.requiredParams.push(a):i.optionalParams.push(a)},g=function(t){var e,r,n;for(e=0,r=t.length;r>e;e++)n=t[e],i.parts.push(n)};m>u;)if(r=t.charAt(u++),r===l)h?(++f,p+=r):(s=this._parseParams(p,e),d(s.params,s.requiredParams),g(s.parts),p="",f=0,h=!0);else if(r===c)if(h)if(0===f){for(s=this._parsePattern(p,!0),p={what:"optional",dependOnParams:[],parts:s.parts},d(s.params,s.requiredParams),g([p]),n=0,a=p.parts.length;a>n;++n)o=p.parts[n]&&p.parts[n].what,"param"===o?p.dependOnParams.push(p.parts[n].name):"optional"===o&&p.dependOnParams.push.apply(p.dependOnParams,p.parts[n].dependOnParams);p="",h=!1}else--f,p+=r;else p+=r;else p+=r;return s=this._parseParams(p,e),d(s.params,s.requiredParams),g(s.parts),i},t.prototype._parseParams=function(t,e){var r,n,a,o,s,i=t.match(y);if(s={parts:[],params:[],requiredParams:[]},i)for(r=0,n=i.length;n>r;++r)a=i[r],a.charAt(0)===f&&a.charAt(a.length-1)===h?(o=a.substr(1,a.length-2),s.params.push(o),s.parts.push({what:"param",name:o}),e||s.requiredParams.push(o)):s.parts.push(a);return s},t.prototype._buildParseRegExp=function(t){t.parseRegExpSource="^"+this._buildParseRegExpParts(t.parts)+"$",t.parseRegExp=new RegExp(t.parseRegExpSource)},t.prototype._buildParseRegExpParts=function(t){var e,r,n,a="";for(e=0,r=t.length;r>e;++e)n=t[e],a+="string"==typeof n?p(n):"param"===n.what?"("+(this._getParamValueRegExpSource(n.name)||d)+")":"(?:"+this._buildParseRegExpParts(n.parts)+")?";return a},t.prototype._getParamValueRegExpSource=function(t){var e,r,n=this._conditionRegExpSources||(this._conditionRegExpSources={}),o=this._conditions;return a(n,t)||(a(o,t)?(r=o[t],e=s(r)?"(?:"+r.join("|")+")":r+""):e=null,n[t]=e),n[t]},t.prototype._getParamValueRegExp=function(t){var e,r=this._conditionRegExps||(this._conditionRegExps={});return a(r,t)||(e=this._getParamValueRegExpSource(t),r[t]=e?new RegExp("^"+e+"$"):null),r[t]},t.prototype._checkParamValue=function(t,e){var r=this._getParamValueRegExp(t);return r?r.test(e):!0},t.prototype._buildBuildFn=function(t){t.buildFnSource="var h=({}).hasOwnProperty;return "+this._buildBuildFnParts(t.parts)+";",t.buildFn=new Function("p",t.buildFnSource)},t.prototype._buildBuildFnParts=function(t){var e,r,n,o,s,i,u='""',f=this._options.defaults;for(e=0,r=t.length;r>e;++e)if(s=t[e],"string"==typeof s)u+='+"'+p(s)+'"';else if("param"===s.what)u+='+(h.call(p,"'+p(s.name)+'")?'+'p["'+p(s.name)+'"]:'+(f&&a(f,s.name)?'"'+p(f[s.name])+'"':'""')+")";else{for(u+="+((false",n=0,o=s.dependOnParams.length;o>n;++n)i=s.dependOnParams[n],u+='||(h.call(p,"'+p(i)+'")'+(f&&a(f,i)?'&&p["'+p(i)+'"]!=="'+p(f[i])+'"':"")+")";u+=")?("+this._buildBuildFnParts(s.parts)+'):"")'}return u},t.prototype._isDataMatched=function(t){var e,r=this._options.data;if("function"==typeof t)return Boolean(t(r));if(t&&"object"==typeof t)for(e in t)if(a(t,e)&&(!r||"object"!=typeof r||r[e]!==t[e]))return!1;return!0},t.prototype.match=function(t,e){var r,n,o,p,u,f,h,l,c,m,d=null,g=this._options,_=g.postMatch,P=g.defaults,y=this._patterns;if("string"!=typeof t||e&&!this._isDataMatched(e))return d;for(m in y)if(a(y,m)&&(c=y[m],r=t.match(c.parseRegExp))){l=c;break}if(r){for(d={},n=1,o=r.length;o>n;++n)if("undefined"!=typeof r[n]&&""!==r[n])if(p=l.params[n-1],p===E)h=r[n];else if(p===b){if(t.charAt(t.length-2)===x)return null}else d[p]=r[n];if(h&&g.useQueryString!==!1){f=i.parse(h);for(p in f)if(a(f,p)&&!a(d,p))if(u=f[p],l.mainParams[p]&&s(u)&&(u=u[0]),s(u))for(d[p]=[],n=0,o=u.length;o>n;++n)this._checkParamValue(p,u[n])&&d[p].push(u[n]);else this._checkParamValue(p,u)&&(d[p]=u)}for(p in P)a(P,p)&&!a(d,p)&&(d[p]=P[p])}return d&&"function"==typeof _&&(d=_(d),d&&"object"==typeof d||(d=null)),d},t.prototype._getPatternParamsIntersection=function(t,e){var r,n,o,s=[];for(r=0,n=t.length;n>r;r++)o=t[r],a(e,o)&&s.push(o);return s},t.prototype._getPatternForBuild=function(t,e){var r,n,o,s,i,p,u,f,h=this._patterns,l=0,c=null;for(n in h)if(a(h,n)){if(o=h[n],e){if(i=o.requiredParams,p=i.length,f=this._getPatternParamsIntersection(i,t),p!==f.length)continue;u=o.optionalParams,f=this._getPatternParamsIntersection(u,t),r=f.length*g+p*_}else s=o.params,f=this._getPatternParamsIntersection(s,t),r=f.length*_;r||(r=P),r>l&&(l=r,c=o)}return c},t.prototype.build=function(t,e){var r,n,o,s,p,u=this._options,f={},h=u.useQueryString!==!1,l={},c=u.preBuild;if(p=t||{},"function"==typeof c&&(p=c(p)),n=this._getPatternForBuild(p,e),!n)return null;for(o in p)if(a(p,o)&&null!==p[o]&&"undefined"!=typeof p[o]&&(n.mainParams[o]||h)){if(s=p[o],e&&!this._checkParamValue(o,s))return null;(n.mainParams[o]?f:l)[o]=s}return h&&(r=i.stringify(l),r&&(f[E]=r)),n.buildFn(f)},t.prototype.getData=function(){return this._options.data},t.prototype.getName=function(){return this._options.name},r.exports=t,r.exports},"./router":function(){function t(){return this instanceof t?(this._routes=[],this._routesByName={},void 0):new t}var r={},n=e("./route");return t.prototype.addRoute=function(t){var e,r;return e=new n(t),this._routes.push(e),r=e.getName(),r&&(this._routesByName[r]=e),e},t.prototype.find=function(){var t,e,r,n=[],a=this._routes;for(e=0,r=a.length;r>e;++e)t=a[e].match.apply(a[e],arguments),null!==t&&n.push([a[e],t]);return n},t.prototype.findFirst=function(){var t,e,r,n=this._routes;for(e=0,r=n.length;r>e;++e)if(t=n[e].match.apply(n[e],arguments),null!==t)return[n[e],t];return null},t.prototype.getRouteByName=function(t){return this._routesByName[t]||null},t.Route=n,r.exports=t,r.exports}}[t]()}var r=e("./router"),n=!0;"object"==typeof exports&&"string"!=typeof exports.nodeName&&(module.exports=r,n=!1),t.modules&&modules.define&&modules.require&&(modules.define("susanin",function(t){t(r)}),n=!1),"function"==typeof t.define&&define.amd&&(define(function(){return r}),n=!1),n&&(t.Susanin=r)}(this); \ No newline at end of file diff --git a/lib/route.js b/lib/route.js index d195688..6706be4 100644 --- a/lib/route.js +++ b/lib/route.js @@ -75,6 +75,10 @@ var PARAM_NAME_REGEXP_SOURCE = '[a-zA-Z_][\\w\\-]*'; */ var PARAM_VALUE_REGEXP_SOURCE = '[\\w\\-\\.~]+'; +var OPTIONAL_PARAM_WEIGHT = 1; +var REQUIRED_PARAM_WEIGHT = 2; +var MINIMAL_PATTERN_WEIGHT = 1; + /** * @const * @type {RegExp} @@ -108,6 +112,12 @@ var TRAILING_SLASH_PARAM_VALUE = '/'; */ var TRAILING_SLASH_PARAM_VALUE_ESCAPED = escape('/'); +/** + * @const + * @type {String} + */ +var DEFAULT_PATTERN_NAME = 'pattern_' + EXPANDO; + /** * @const * @type {String} @@ -137,48 +147,94 @@ function Route(options) { return new Route(options); } - typeof options === 'string' && (options = { pattern : options }); + this._setOptions(options); +} - if ( ! options || typeof options !== 'object') { - throw new Error('You must specify options'); +Route.prototype._setOptions = function(opts) { + var options = opts; + + if (typeof options === 'string') { + options = { pattern : options }; } - if (typeof options.pattern !== 'string') { - throw new Error('You must specify the pattern of the route'); + if (! options || typeof options !== 'object') { + throw new Error('You must specify options'); } - /** - * @type {RouteOptions} - * @private - */ this._options = options; - this._conditions = options.conditions && typeof options.conditions === 'object' ? options.conditions : {}; + this._setConditions(this._options.conditions); + this._setPatterns(this._options.pattern); +}; + +Route.prototype._setConditions = function(conditions) { + var opts = this._options; - if (options.isTrailingSlashOptional !== false) { - options.pattern += GROUP_OPENED_CHAR + PARAM_OPENED_CHAR + - TRAILING_SLASH_PARAM_NAME + - PARAM_CLOSED_CHAR + GROUP_CLOSED_CHAR; + this._conditions = conditions && typeof conditions === 'object' ? conditions : {}; + + if (opts.isTrailingSlashOptional !== false) { this._conditions[TRAILING_SLASH_PARAM_NAME] = TRAILING_SLASH_PARAM_VALUE_ESCAPED; } - options.pattern += GROUP_OPENED_CHAR + - '?' + PARAM_OPENED_CHAR + QUERY_STRING_PARAM_NAME + PARAM_CLOSED_CHAR + - GROUP_CLOSED_CHAR; this._conditions[QUERY_STRING_PARAM_NAME] = '.*'; +}; - this._paramsMap = []; - this._mainParamsMap = {}; - this._requiredParams = []; +Route.prototype._setPatterns = function(patternOpts) { + var opts = this._options, + defaultPattern, + patterns, + patternIndex, + patternsSize, + currentPattern, + patternName, + patternTest, + parsedPattern, + pattern; + + defaultPattern = { + name : DEFAULT_PATTERN_NAME, + test : patternOpts + }; + patterns = isArray(patternOpts) ? patternOpts : [ defaultPattern ]; - /** - * @type {Array} - * @private - */ - this._parts = this._parsePattern(options.pattern); + this._patterns = {}; + this._patternNames = []; - this._buildParseRegExp(); - this._buildBuildFn(); -} + for (patternIndex = 0, patternsSize = patterns.length; patternIndex < patternsSize; patternIndex++) { + currentPattern = patterns[patternIndex]; + patternName = currentPattern.name; + patternTest = currentPattern.test; + + if (typeof patternTest !== 'string' || typeof patternName !== 'string') { + throw new Error('You must specify the pattern of the route'); + } + + if (opts.isTrailingSlashOptional !== false) { + patternTest += GROUP_OPENED_CHAR + PARAM_OPENED_CHAR + + TRAILING_SLASH_PARAM_NAME + + PARAM_CLOSED_CHAR + GROUP_CLOSED_CHAR; + } + + patternTest += GROUP_OPENED_CHAR + + '?' + PARAM_OPENED_CHAR + QUERY_STRING_PARAM_NAME + PARAM_CLOSED_CHAR + + GROUP_CLOSED_CHAR; + + parsedPattern = this._parsePattern(patternTest); + pattern = { + name : patternName, + test : patternTest, + params : parsedPattern.params, + mainParams : parsedPattern.mainParams, + requiredParams : parsedPattern.requiredParams, + optionalParams : parsedPattern.optionalParams, + parts : parsedPattern.parts + }; + + this._buildParseRegExp(pattern); + this._buildBuildFn(pattern); + this._patterns[patternName] = pattern; + this._patternNames.push(patternName); + } +}; /** * @param {String} pattern @@ -187,14 +243,54 @@ function Route(options) { * @private */ Route.prototype._parsePattern = function(pattern, isOptional) { - var parts = [], - part = '', + var part = '', character, i = 0, j, size, countOpened = 0, isFindingClosed = false, length = pattern.length, - what; + what, + parsedPattern, + parseResult; + + parseResult = { + parts : [], + params : [], + mainParams : [], + requiredParams : [], + optionalParams : [] + }; + + var addParams = function(params, requiredParams) { + var paramIndex, + paramsSize, + param; + + for (paramIndex = 0, paramsSize = params.length; paramIndex < paramsSize; paramIndex++) { + param = params[paramIndex]; + + parseResult.params.push(param); + parseResult.mainParams[param] = true; + + if (requiredParams.indexOf(param) !== -1) { + parseResult.requiredParams.push(param); + } else { + parseResult.optionalParams.push(param); + } + } + }; + + var addParts = function(parts) { + var partIndex, + partsSize, + part; + + for (partIndex = 0, partsSize = parts.length; partIndex < partsSize; partIndex++) { + part = parts[partIndex]; + + parseResult.parts.push(part); + } + }; while (i < length) { character = pattern.charAt(i++); @@ -204,7 +300,10 @@ Route.prototype._parsePattern = function(pattern, isOptional) { ++countOpened; part += character; } else { - this._parseParams(part, parts, isOptional); + parsedPattern = this._parseParams(part, isOptional); + addParams(parsedPattern.params, parsedPattern.requiredParams); + addParts(parsedPattern.parts); + part = ''; countOpened = 0; isFindingClosed = true; @@ -212,13 +311,16 @@ Route.prototype._parsePattern = function(pattern, isOptional) { } else if (character === GROUP_CLOSED_CHAR) { if (isFindingClosed) { if (countOpened === 0) { + parsedPattern = this._parsePattern(part, true); + part = { what : 'optional', dependOnParams : [], - parts : this._parsePattern(part, true) + parts : parsedPattern.parts }; - parts.push(part); + addParams(parsedPattern.params, parsedPattern.requiredParams); + addParts([ part ]); for (j = 0, size = part.parts.length; j < size; ++j) { what = part.parts[j] && part.parts[j].what; @@ -244,22 +346,30 @@ Route.prototype._parsePattern = function(pattern, isOptional) { } } - this._parseParams(part, parts, isOptional); + parsedPattern = this._parseParams(part, isOptional); + addParams(parsedPattern.params, parsedPattern.requiredParams); + addParts(parsedPattern.parts); - return parts; + return parseResult; }; /** * @param {String} pattern - * @param {Array} parts * @param {Boolean} isOptional * @private */ -Route.prototype._parseParams = function(pattern, parts, isOptional) { +Route.prototype._parseParams = function(pattern, isOptional) { var matches = pattern.match(PARSE_PARAMS_REGEXP), i, size, part, - paramName; + paramName, + parseResult; + + parseResult = { + parts : [], + params : [], + requiredParams : [] + }; if (matches) { for (i = 0, size = matches.length; i < size; ++i) { @@ -267,27 +377,28 @@ Route.prototype._parseParams = function(pattern, parts, isOptional) { if (part.charAt(0) === PARAM_OPENED_CHAR && part.charAt(part.length - 1) === PARAM_CLOSED_CHAR) { paramName = part.substr(1, part.length - 2); - this._paramsMap.push(paramName); - this._mainParamsMap[paramName] = true; - isOptional || this._requiredParams.push(paramName); - parts.push({ + parseResult.params.push(paramName); + parseResult.parts.push({ what : 'param', name : paramName }); + isOptional || parseResult.requiredParams.push(paramName); } else { - parts.push(part); + parseResult.parts.push(part); } } } + + return parseResult; }; /** * @private */ -Route.prototype._buildParseRegExp = function() { - this._parseRegExpSource = '^' + this._buildParseRegExpParts(this._parts) + '$'; - this._parseRegExp = new RegExp(this._parseRegExpSource); +Route.prototype._buildParseRegExp = function(pattern) { + pattern.parseRegExpSource = '^' + this._buildParseRegExpParts(pattern.parts) + '$'; + pattern.parseRegExp = new RegExp(pattern.parseRegExpSource); }; /** @@ -374,10 +485,10 @@ Route.prototype._checkParamValue = function(paramName, paramValue) { /** * @private */ -Route.prototype._buildBuildFn = function() { - this._buildFnSource = 'var h=({}).hasOwnProperty;return ' + this._buildBuildFnParts(this._parts) + ';'; +Route.prototype._buildBuildFn = function(pattern) { + pattern.buildFnSource = 'var h=({}).hasOwnProperty;return ' + this._buildBuildFnParts(pattern.parts) + ';'; /*jshint evil:true */ - this._buildFn = new Function('p', this._buildFnSource); + pattern.buildFn = new Function('p', pattern.buildFnSource); }; /** @@ -397,7 +508,6 @@ Route.prototype._buildBuildFnParts = function(parts) { if (typeof part === 'string') { ret += '+"' + escape(part) + '"' ; } else if (part.what === 'param') { - this._mainParamsMap[part.name] = true; ret += '+(h.call(p,"' + escape(part.name) + '")?' + 'p["' + escape(part.name) + '"]:' + (defaults && has(defaults, part.name) ? @@ -465,20 +575,34 @@ Route.prototype.match = function(path, data) { queryString, options = this._options, filter = options.postMatch, - defaults = options.defaults; + defaults = options.defaults, + patterns = this._patterns, + currentPattern, + pattern, + patternName; if (typeof path !== 'string' || (data && ! this._isDataMatched(data))) { return ret; } - matches = path.match(this._parseRegExp); + for (patternName in patterns) { + if (has(patterns, patternName)) { + pattern = patterns[patternName]; + matches = path.match(pattern.parseRegExp); + + if (matches) { + currentPattern = pattern; + break; + } + } + } if (matches) { ret = {}; for (i = 1, size = matches.length; i < size; ++i) { if (typeof matches[i] !== 'undefined' && /* for IE lt 9*/ matches[i] !== '') { - paramName = this._paramsMap[i - 1]; + paramName = currentPattern.params[i - 1]; if (paramName === QUERY_STRING_PARAM_NAME) { queryString = matches[i]; } else if (paramName === TRAILING_SLASH_PARAM_NAME) { @@ -496,7 +620,7 @@ Route.prototype.match = function(path, data) { for (paramName in queryParams) { if (has(queryParams, paramName) && ! has(ret, paramName)) { paramValue = queryParams[paramName]; - if (this._mainParamsMap[paramName] && isArray(paramValue)) { + if (currentPattern.mainParams[paramName] && isArray(paramValue)) { paramValue = paramValue[0]; } @@ -531,6 +655,74 @@ Route.prototype.match = function(path, data) { return ret; }; +Route.prototype._getPatternParamsIntersection = function(patternParams, params) { + var intersection = [], + paramIndex, + size, + paramName; + + for (paramIndex = 0, size = patternParams.length; paramIndex < size; paramIndex++) { + paramName = patternParams[paramIndex]; + + if (has(params, paramName)) { + intersection.push(paramName); + } + } + + return intersection; +}; + +Route.prototype._getPatternForBuild = function(params, isStrict) { + var patterns = this._patterns, + maxPatternWeight = 0, + patternWeight, + patternName, + pattern, + patternParams, + requiredParams, + requiredParamsCount, + optionalParams, + paramsIntersection, + patternForBuild = null; + + for (patternName in patterns) { + if (has(patterns, patternName)) { + pattern = patterns[patternName]; + + if (isStrict) { + requiredParams = pattern.requiredParams; + requiredParamsCount = requiredParams.length; + paramsIntersection = this._getPatternParamsIntersection(requiredParams, params); + + if (requiredParamsCount !== paramsIntersection.length) { + continue; + } + + optionalParams = pattern.optionalParams; + paramsIntersection = this._getPatternParamsIntersection(optionalParams, params); + patternWeight = paramsIntersection.length * OPTIONAL_PARAM_WEIGHT + + requiredParamsCount * REQUIRED_PARAM_WEIGHT; + } else { + patternParams = pattern.params; + paramsIntersection = this._getPatternParamsIntersection(patternParams, params); + + patternWeight = paramsIntersection.length * REQUIRED_PARAM_WEIGHT; + } + + if (! patternWeight) { + patternWeight = MINIMAL_PATTERN_WEIGHT; + } + + if (patternWeight > maxPatternWeight) { + maxPatternWeight = patternWeight; + patternForBuild = pattern; + } + } + } + + return patternForBuild; +}; + /** * Build path from params * @param {Object} params @@ -543,36 +735,37 @@ Route.prototype.build = function(params, isStrict) { useQueryString = options.useQueryString !== false, queryParams = {}, queryString, + pattern, paramName, paramValue, filter = options.preBuild, - i, size; + buildParams; + + buildParams = params || {}; if (typeof filter === 'function') { - params = filter(params); + buildParams = filter(buildParams); + } + + pattern = this._getPatternForBuild(buildParams, isStrict); + + if (! pattern) { + return null; } - for (paramName in params) { + for (paramName in buildParams) { if ( - has(params, paramName) && - params[paramName] !== null && - typeof params[paramName] !== 'undefined' && - (this._mainParamsMap[paramName] || useQueryString) + has(buildParams, paramName) && + buildParams[paramName] !== null && + typeof buildParams[paramName] !== 'undefined' && + (pattern.mainParams[paramName] || useQueryString) ) { - paramValue = params[paramName]; + paramValue = buildParams[paramName]; if (isStrict && ! this._checkParamValue(paramName, paramValue)) { return null; } - (this._mainParamsMap[paramName] ? newParams : queryParams)[paramName] = paramValue; - } - } - - if (isStrict) { - for (i = 0, size = this._requiredParams.length; i < size; ++i) { - if ( ! has(newParams, this._requiredParams[i])) { - return null; - } + (pattern.mainParams[paramName] ? newParams : queryParams)[paramName] = paramValue; } } @@ -581,7 +774,7 @@ Route.prototype.build = function(params, isStrict) { queryString && (newParams[QUERY_STRING_PARAM_NAME] = queryString); } - return this._buildFn(newParams); + return pattern.buildFn(newParams); }; /** diff --git a/test/route.build.js b/test/route.build.js index 9f25209..2e296ed 100644 --- a/test/route.build.js +++ b/test/route.build.js @@ -182,4 +182,44 @@ describe('route.build()', function() { done(); }); + it('Support multiple patterns', function(done) { + var route = Route({ + pattern : [ + { name : 'search', test : '/search' }, + { name : 'search1', test : '/search/mark-' }, + { name : 'search2', test : '/search/mark-/year-' }, + { name : 'search3', test : '/search/mark-(/)' }, + { name : 'search4', test : '/search/mark-/' }, + { name : 'search5', test : '/search/mark-(/)/year-' }, + { name : 'search6', test : '/search/mark-/price-' }, + ] + }); + + assert.strictEqual(route.build(), '/search'); + assert.strictEqual(route.build({}), '/search'); + assert.strictEqual(route.build({}, true), '/search'); + assert.strictEqual(route.build({ foo : 'bar' }), '/search?foo=bar'); + assert.strictEqual(route.build({ foo : 'bar' }, true), '/search?foo=bar'); + assert.strictEqual(route.build({ model : 'corona' }), '/search/mark-/corona'); + assert.strictEqual(route.build({ model : 'corona' }, true), '/search?model=corona'); + assert.strictEqual(route.build({ year : '1993' }), '/search/mark-/year-1993'); + assert.strictEqual(route.build({ year : '1993' }, true), '/search?year=1993'); + assert.strictEqual(route.build({ model : 'toyota', year : '1993' }), '/search/mark-/toyota/year-1993'); + assert.strictEqual(route.build({ model : 'toyota', year : '1993' }, true), '/search?model=toyota&year=1993'); + assert.strictEqual(route.build({ mark : 'toyota' }), '/search/mark-toyota'); + assert.strictEqual(route.build({ mark : 'toyota' }, true), '/search/mark-toyota'); + assert.strictEqual(route.build({ mark : 'toyota', foo : 'bar' }), '/search/mark-toyota?foo=bar'); + assert.strictEqual(route.build({ mark : 'toyota', foo : 'bar' }, true), '/search/mark-toyota?foo=bar'); + assert.strictEqual(route.build({ mark : 'toyota', year : 1993 }), '/search/mark-toyota/year-1993'); + assert.strictEqual(route.build({ mark : 'toyota', year : 1993 }, true), '/search/mark-toyota/year-1993'); + assert.strictEqual(route.build({ mark : 'toyota', model : 'corona' }), '/search/mark-toyota/corona'); + assert.strictEqual(route.build({ mark : 'toyota', model : 'corona' }, true), '/search/mark-toyota/corona'); + assert.strictEqual(route.build({ mark : 'toyota', model : 'corona', year : 1993 }), '/search/mark-toyota/corona/year-1993'); + assert.strictEqual(route.build({ mark : 'toyota', model : 'corona', year : 1993 }, true), '/search/mark-toyota/corona/year-1993'); + assert.strictEqual(route.build({ mark : 'toyota', price : 20000 }), '/search/mark-toyota/price-20000'); + assert.strictEqual(route.build({ mark : 'toyota', price : 20000 }, true), '/search/mark-toyota/price-20000'); + + done(); + }); + });