RouteMap holds an internal table of route patterns and method names in addition to some
221 | adding/removing/utility methods and a handler for request routing.
overrides the context where listener methods are sought, the default scope is window
269 | (in a browser setting), returns the current context, if no scope object is passed in, just
270 | returns current context without setting context
this function is fired when no rule is matched by a URL, by default it does nothing, but it could be set up
290 | to handle things like 404 responses on the server-side or bad hash fragments in the browser
URL grabber function, defaults to checking the URL fragment (hash); this function should be
300 | overwritten in a server-side environment; this method is called by RouteMap.handler; without
301 | window.location.hash it will return '/'
in a browser setting, it changes window.location.hash, in other settings, it should be
311 | overwritten to do something useful (if necessary); it will not throw an error if window does
312 | not exist
main handler function for routing, this should be bound to hashchange events in the browser, or
322 | (in conjunction with updating RouteMap.get) used with the HTML5 history API, it detects
323 | all the matching route patterns, parses the URL parameters and fires their methods with the arguments from
324 | the parsed URL; the timing of RouteMap.current and RouteMap.last being set is as follows
325 | (pseudo-code):
326 |
returns the parsed (see #parse) last accessed route; when route listeners are being called,
355 | last is the previously accessed route, after listeners have finished firing, the current parsed
356 | route replaces last's value
like RouteMap.post_add this function can be overwritten to add application-specific code into
402 | route mapping, it is called before a route begins being dispatched to all matching rules; it receives the
403 | list of matching parsed route objects (#parse) and is expected to return it; one application of this
404 | function might be to set application-wide variables like debug flags
if a string is passed in, it overwrites the prefix that is removed from each URL before parsing; primarily
414 | used for hashbang (#!); either way, it returns the current prefix
RouteMap holds an internal table of route patterns and method names in addition to some
450 | adding/removing/utility methods and a handler for request routing.
451 |
It does not have any dependencies and is written in "plain old" JS, but it does require JS 1.8 array methods, so
452 | if the environment it will run in does not have those, the reference implementations from
453 | Mozilla should be
454 | supplied external to this library.
455 |
It is designed to be used in both a browser setting and a server-side context (for example in node.js).
456 | LICENSING INFORMATION:
457 |
458 | Copyright 2011 OpenGamma Inc. and the OpenGamma group of companies
459 | Licensed under the Apache License, Version 2.0 (the "License");
460 | you may not use this file except in compliance with the License.
461 | You may obtain a copy of the License at
462 |
463 | http://www.apache.org/licenses/LICENSE-2.0
464 |
465 | Unless required by applicable law or agreed to in writing, software
466 | distributed under the License is distributed on an "AS IS" BASIS,
467 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
468 | See the License for the specific language governing permissions and
469 | limitations under the License.
470 |
527 | adds a rule to the internal table of routes and methods
528 |
529 |
530 |
531 |
532 |
533 |
534 |
535 |
536 |
Parameters:
537 |
538 |
539 | {Object}rule
540 |
541 |
542 |
rule specification
543 |
544 |
545 | {String}rule.route
546 |
547 |
548 |
route pattern definition; there are three types of pattern arguments: scalars,
549 | keyvals, and stars; scalars are individual values in a URL (all URL values are separate by the
550 | '/' character), keyvals are named values, e.g. 'foo=bar', and star values are wildcards; so for
551 | example, the following pattern represents all the possible options:
552 | '/foo/:id/:sub?/attr:/subattr:?/rest:*'
the ? means that argument is
553 | optional, the star rule is named rest but it could have just simply been left as *,
554 | which means the resultant dictionary would have put the wildcard remainder into args['*']
555 | instead of args.rest; so the following URL would match the pattern above:
696 | overrides the context where listener methods are sought, the default scope is window
697 | (in a browser setting), returns the current context, if no scope object is passed in, just
698 | returns current context without setting context
699 |
700 |
701 |
702 |
703 |
704 |
705 |
706 |
707 |
Parameters:
708 |
709 |
710 | {Object}scope
711 |
712 |
713 |
the scope within which methods for mapped routes will be looked for
714 |
715 |
716 |
717 |
718 |
719 |
720 |
721 |
722 |
Returns:
723 |
724 |
{Object} the current context within which RouteMap searches for handlers
741 | returns the parsed (see #parse) currently accessed route; after listeners have finished
742 | firing, current and last are the same
743 |
744 |
745 |
781 | this function is fired when no rule is matched by a URL, by default it does nothing, but it could be set up
782 | to handle things like 404 responses on the server-side or bad hash fragments in the browser
783 |
784 |
785 |
807 | URL grabber function, defaults to checking the URL fragment (hash); this function should be
808 | overwritten in a server-side environment; this method is called by RouteMap.handler; without
809 | window.location.hash it will return '/'
810 |
811 |
812 |
{String} by default, this returns a subset of the URL hash (everything after the first
825 | '/' character ... if nothing follows a slash, it returns '/'); if overwritten, it
826 | must be a function that returns URL path strings (beginning with '/') to match added rules
843 | in a browser setting, it changes window.location.hash, in other settings, it should be
844 | overwritten to do something useful (if necessary); it will not throw an error if window does
845 | not exist
846 |
847 |
848 |
881 | main handler function for routing, this should be bound to hashchange events in the browser, or
882 | (in conjunction with updating RouteMap.get) used with the HTML5 history API, it detects
883 | all the matching route patterns, parses the URL parameters and fires their methods with the arguments from
884 | the parsed URL; the timing of RouteMap.current and RouteMap.last being set is as follows
885 | (pseudo-code):
886 |
927 | returns a URL fragment by applying parameters to a rule; uses #compile and does not catch any errors
928 | thrown by that function
929 |
930 |
931 |
932 |
933 |
934 |
935 |
936 |
937 |
Parameters:
938 |
939 |
940 | {Object}rule
941 |
942 |
943 |
the rule specification; it typically looks like:
944 | {route:'/foo', method:'bar'}
but only route is strictly necessary
945 |
946 |
947 | {Object}params
948 |
949 |
950 |
a dictionary of argument key/value pairs required by the rule
951 |
952 |
953 |
954 |
955 |
956 |
957 |
958 |
Throws:
959 |
960 |
961 | {TypeError}
962 |
963 |
if a required parameter is not present
964 |
965 |
966 |
967 |
968 |
969 |
Returns:
970 |
971 |
{String} URL fragment resulting from applying arguments to rule pattern
988 | returns the parsed (see #parse) last accessed route; when route listeners are being called,
989 | last is the previously accessed route, after listeners have finished firing, the current parsed
990 | route replaces last's value
991 |
992 |
993 |
1184 | and it is expected to pass back an object of the same format; it can be overwritten to post-process added
1185 | rules e.g. to add extra default application-wide parameters; by default, it simply returns what was passed
1186 | into it
1187 |
1188 |
1189 |
1190 |
1191 |
1192 |
1193 |
1194 |
1195 |
Parameters:
1196 |
1197 |
1198 | {Object}compiled
1199 |
1200 |
1201 |
the compiled rule
1202 |
1203 |
1204 |
1205 |
1206 |
1207 |
1208 |
1209 |
1210 |
Returns:
1211 |
1212 |
{Object} the default function returns the exact object it received; a custom function needs to
1213 | an object that is of the same form (but could possibly have more or fewer parameters, etc.)
1230 | like RouteMap.post_add this function can be overwritten to add application-specific code into
1231 | route mapping, it is called before a route begins being dispatched to all matching rules; it receives the
1232 | list of matching parsed route objects (#parse) and is expected to return it; one application of this
1233 | function might be to set application-wide variables like debug flags
1234 |
1235 |
1236 |
1237 |
1238 |
1239 |
1240 |
1241 |
1242 |
Parameters:
1243 |
1244 |
1245 | {Array}parsed
1246 |
1247 |
1248 |
the parsed request
1249 |
1250 |
1251 |
1252 |
1253 |
1254 |
1255 |
1256 |
1257 |
Returns:
1258 |
1259 |
{Array} a list of the same form as the one it receives
1276 | if a string is passed in, it overwrites the prefix that is removed from each URL before parsing; primarily
1277 | used for hashbang (#!); either way, it returns the current prefix
1278 |
1279 |
1280 |
1313 | counterpart to RouteMap.add, removes a rule specification; * remove uses
1314 | #compile and does not catch any errors thrown by that function
1315 |
1316 |
1317 |
1318 |
1319 |
1320 |
1321 |
1322 |
1323 |
Parameters:
1324 |
1325 |
1326 | {Object}rule
1327 |
1328 |
1329 |
the rule specification that was used in RouteMap.add
1330 |
1331 |
1332 |
1333 |
1334 |
1335 |
1336 |
1337 |
Throws:
1338 |
1339 |
1340 | {TypeError}
1341 |
1342 |
if rule.route or rule.method are not strings or empty strings
1363 |
1364 | Documentation generated by JsDoc Toolkit 2.4.0 on Wed Apr 18 2012 13:03:13 GMT+0100 (BST)
1365 |
1366 |
1367 |
1368 |
--------------------------------------------------------------------------------
/routemap.compressed.js:
--------------------------------------------------------------------------------
1 | (function(x,y){(function(a,b){if(!a.every||!a.filter||!a.indexOf||!a.map||!a.reduce||!a.some||!a.forEach)throw Error("See "+b+" for reference versions of Array.prototype methods available in JS 1.8");})([],"https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/");var k,f={},l={},i=[],p=0,n=0,u=encodeURIComponent,v=decodeURIComponent,o="#",z=/\*|:|\?/,A=/(^([^\*:\?]+):\*)|(^\*$)/,B=/^:([^\*:\?]+)(\??)$/,C=/^([^\*:\?]+):(\??)$/,D=/([^/])$/,q="undefined"!==typeof window?window:{},
2 | h=function(a){return"string"!==typeof a||!a.length},r=function(){var a=Object.prototype.toString,b=function(c){return"object"!==typeof c||null===c?c:"[object Array]"===a.call(c)?c.map(b):r(c)};return Array.prototype.reduce.call(arguments,function(c,d){if(!d||"object"!==typeof d||"[object Array]"===a.call(d))throw new TypeError("merge: "+a.call(d)+" is not mergeable");for(var e in d)d.hasOwnProperty(e)&&(c[e]=b(d[e]));return c},{})},w=function(a){var b=i.filter(function(b){return 0===a.replace(D,"$1/").indexOf(b)}).filter(function(a,
3 | b){return!b||f[a].some(function(a){return!!a.rules.star})});return!b.length?[]:b.reduce(function(b,d){var e=f[d].map(function(b){var c={},e=b.rules.scalars,m=b.rules.keyvals,f,g=a.replace(d,"").split("/").reduce(function(a,b){var c=b.split("="),d=c[0],c=c.slice(1).join("=");return!b.length||(c?a.keyvals[d]=c:a.scalars.push(b)),a},{keyvals:{},scalars:[]}),j,h=m.reduce(function(a,b){return(a[b.name]=0)||a},{}),i=e.filter(function(a){return a.required}).length,l=m.filter(function(a){return a.required}).every(function(a){return g.keyvals.hasOwnProperty(a.name)});
4 | if(i>g.scalars.length||!l)return 0;if(!b.rules.star){if(g.scalars.length>e.length)return 0;for(j in g.keyvals)if(g.keyvals.hasOwnProperty(j)&&!h.hasOwnProperty(j))return 0}g.scalars.slice(0,e.length).forEach(function(a,b){c[e[b].name]=v(a)});m.forEach(function(a){g.keyvals[a.name]&&(c[a.name]=v(g.keyvals[a.name]));delete g.keyvals[a.name]});if(b.rules.star){m=g.scalars.slice(e.length,g.scalars.length);for(j in g.keyvals)g.keyvals.hasOwnProperty(j)&&m.push([j,g.keyvals[j]].join("="));c[b.rules.star]=
5 | m.join("/")}try{if(f=b.method.split(".").reduce(function(a,b){return a[b]},q),"function"!==typeof f)throw Error();}catch(n){throw new TypeError("parse: "+b.method+" is not a function in current context");}return{page:d,hash:k.hash({route:b.raw},c),method:f,args:c}});return b.concat(e).filter(Boolean)},[]).sort(function(a,b){return b.hash.length-a.hash.length})},t=function(a){return function(b){var c,d={},e="/"===b[0]?b:~(c=b.indexOf("/"))?b.slice(c):0,s=function(a){if(d.hasOwnProperty(a)||(d[a]=0))throw new SyntaxError('compile: "'+
6 | a+'" is repeated in: '+b);};if(!e)throw new SyntaxError("compile: the route "+b+" was not understood");if(a[e])return a[e];c=e.split("/").reduce(function(a,c){var d=a.rules,e=d.scalars,g=d.keyvals;if(d.star)throw new SyntaxError("compile: no rules can follow a * directive in: "+b);if(!~c.search(z)&&!e.length&&!g.length)return a.page.push(c),a;if(c.match(A))return d.star=RegExp.$2||RegExp.$3,s(d.star),a;if(c.match(B)){if(a.has_optional_scalar)throw new SyntaxError('compile: "'+c+'" cannot follow an optional rule in: '+
7 | b);RegExp.$2&&(a.has_optional_scalar=c);return e.push({name:RegExp.$1,required:!RegExp.$2}),s(RegExp.$1),a}if(c.match(C))return g.push({name:RegExp.$1,required:!RegExp.$2}),s(RegExp.$1),a;throw new SyntaxError('compile: the rule "'+c+'" was not understood in: '+b);},{page:[],rules:{scalars:[],keyvals:[],star:!1},has_optional_scalar:""});delete c.has_optional_scalar;c.page=c.page.join("/").replace(/\/$/,"")||"/";return a[e]=c}}({});x[y]=k={add:function(a){var b=a.method,c=a.route,d=[a.method,a.route].join("|");
8 | if([c,b].some(h))throw new TypeError("add: rule.route and rule.method must both be non-empty strings");if(l[d])throw Error("add: "+c+" to "+b+" already exists");a=t(c);l[d]=!0;if(!f[a.page]&&(f[a.page]=[]))i=i.concat(a.page).sort(function(a,b){return b.length-a.length});f[a.page].push(k.post_add({method:b,rules:a.rules,raw:c}))},context:function(a){return q="object"===typeof a?a:q},current:function(){return n?r(n):null},default_handler:function(){},get:function(){if("undefined"===typeof window)return"/";
9 | var a=window.location.hash,b=a.indexOf("/");return~b?a.slice(b):"/"},go:function(a){"undefined"!==typeof window&&(window.location.hash=(0===a.indexOf(o)?"":o)+a)},handler:function(){var a=k.get(),b=w(a),c=Array.prototype.slice.call(arguments);if(!b.length)return k.default_handler.apply(null,[a].concat(c));n=b[0];b=k.pre_dispatch(b);n=b[0];b.forEach(function(a){a.method.apply(null,[a.args].concat(c))});p=b[0]},hash:function(a,b){var c,d,b=b||{};if(h(a.route))throw new TypeError("hash: rule.route must be a non-empty string");
10 | d=t(a.route);c=d.page+("/"===d.page?"":"/")+d.rules.scalars.map(function(c){var d=u(b[c.name]),f=void 0===b[c.name]||h(d);if(c.required&&f)throw new TypeError("hash: params."+c.name+" is undefined, route: "+a.route);return f?0:d}).concat(d.rules.keyvals.map(function(c){var d=u(b[c.name]),f=void 0===b[c.name]||h(d);if(c.required&&f)throw new TypeError("hash: params."+c.name+" is undefined, route: "+a.route);return f?0:c.name+"="+d})).filter(Boolean).join("/");d.rules.star&&b[d.rules.star]&&(c+=("/"===
11 | c[c.length-1]?"":"/")+b[d.rules.star]);return c},last:function(){return p?r(p):null},parse:function(a){var b;b=a.indexOf("/");a=~b?a.slice(b):"";if(h(a))throw new TypeError("parse: hash must be a string with a / character");if(!(b=w(a)).length)throw new SyntaxError("parse: "+a+" cannot be parsed");return{page:b[0].page,args:b[0].args}},post_add:function(a){return a},pre_dispatch:function(a){return a},prefix:function(a){return o="undefined"!==typeof a?a+"":o},remove:function(a){var b=a.method,c=a.route,
12 | d=[a.method,a.route].join("|"),e;if([c,b].some(h))throw new TypeError("remove: rule.route and rule.method must both be non-empty strings");l[d]&&(a=t(c),delete l[d],f[a.page]=f[a.page].filter(function(a){return a.raw!==c||a.method!==b}),!f[a.page].length&&delete f[a.page]&&~(e=i.indexOf(a.page))&&i.splice(e,1))}}})("undefined"===typeof exports?window:exports,"RouteMap");
13 |
--------------------------------------------------------------------------------
/routemap.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
RouteMap holds an internal table of route patterns and method names in addition to some
3 | * adding/removing/utility methods and a handler for request routing.
4 | *
It does not have any dependencies and is written in "plain old" JS, but it does require JS 1.8 array methods, so
5 | * if the environment it will run in does not have those, the reference implementations from
6 | * Mozilla should be
7 | * supplied external to this library.
8 | *
It is designed to be used in both a browser setting and a server-side context (for example in node.js).
9 | * LICENSING INFORMATION:
10 | *
11 | * Copyright 2011 OpenGamma Inc. and the OpenGamma group of companies
12 | * Licensed under the Apache License, Version 2.0 (the "License");
13 | * you may not use this file except in compliance with the License.
14 | * You may obtain a copy of the License at
15 | *
16 | * http://www.apache.org/licenses/LICENSE-2.0
17 | *
18 | * Unless required by applicable law or agreed to in writing, software
19 | * distributed under the License is distributed on an "AS IS" BASIS,
20 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 | * See the License for the specific language governing permissions and
22 | * limitations under the License.
23 | *
24 | * @see OpenGamma
25 | * @see Apache License, Version 2.0
26 | * @see Mozilla Developer
27 | * Network
28 | * @name RouteMap
29 | * @namespace RouteMap
30 | * @author Afshin Darian
31 | * @static
32 | * @throws {Error} if JS 1.8 Array.prototype methods don't exist
33 | */
34 | (function (pub, namespace) { // defaults to exports, uses window if exports does not exist
35 | (function (arr, url) { // plain old JS, but needs some JS 1.8 array methods
36 | if (!arr.every || !arr.filter || !arr.indexOf || !arr.map || !arr.reduce || !arr.some || !arr.forEach)
37 | throw new Error('See ' + url + ' for reference versions of Array.prototype methods available in JS 1.8');
38 | })([], 'https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/');
39 | var routes /* internal reference to RouteMap */, active_routes = {}, added_routes = {}, flat_pages = [],
40 | last = 0, current = 0, encode = encodeURIComponent, decode = decodeURIComponent, has = 'hasOwnProperty',
41 | EQ = '=' /* equal string */, SL = '/' /* slash string */, PR = '#' /* default prefix string */,
42 | token_exp = /\*|:|\?/, star_exp = /(^([^\*:\?]+):\*)|(^\*$)/, scalar_exp = /^:([^\*:\?]+)(\??)$/,
43 | keyval_exp = /^([^\*:\?]+):(\??)$/, slash_exp = new RegExp('([^' + SL + '])$'),
44 | context = typeof window !== 'undefined' ? window : {}, // where listeners reside, routes.context() overwrites it
45 | /** @ignore */
46 | invalid_str = function (str) {return typeof str !== 'string' || !str.length;},
47 | /** @ignore */
48 | fingerprint = function (rule) {return [rule.method, rule.route].join('|');},
49 | /**
50 | * merges one or more objects into a new object by value (nothing is a reference), useful for cloning
51 | * @name RouteMap#merge
52 | * @inner
53 | * @function
54 | * @type Object
55 | * @returns {Object} a merged object
56 | * @throws {TypeError} if one of the arguments is not a mergeable object (i.e. a primitive, null or array)
57 | */
58 | merge = function () {
59 | var self = 'merge', to_string = Object.prototype.toString, clone = function (obj) {
60 | return typeof obj !== 'object' || obj === null ? obj // primitives
61 | : to_string.call(obj) === '[object Array]' ? obj.map(clone) // arrays
62 | : merge(obj); // objects
63 | };
64 | return Array.prototype.reduce.call(arguments, function (acc, obj) {
65 | if (!obj || typeof obj !== 'object' || to_string.call(obj) === '[object Array]')
66 | throw new TypeError(self + ': ' + to_string.call(obj) + ' is not mergeable');
67 | for (var key in obj) if (obj[has](key)) acc[key] = clone(obj[key]);
68 | return acc;
69 | }, {});
70 | },
71 | /**
72 | * parses a path and returns a list of objects that contain argument dictionaries, methods, and raw hash values
73 | * @name RouteMap#parse
74 | * @inner
75 | * @function
76 | * @param {String} path
77 | * @type Array
78 | * @returns {Array} a list of parsed objects in descending order of matched hash length
79 | * @throws {TypeError} if the method specified by a rule specification does not exist during parse time
80 | */
81 | parse = function (path) {
82 | // go with the first matching page (longest) or any pages with * rules
83 | var self = 'parse', pages = flat_pages.filter(function (val) { // add slash to paths so all vals match
84 | return path.replace(slash_exp, '$1' + SL).indexOf(val) === 0;
85 | })
86 | .filter(function (page, index) {
87 | return !index || active_routes[page].some(function (val) {return !!val.rules.star;});
88 | });
89 | return !pages.length ? [] : pages.reduce(function (acc, page) { // flatten parsed rules for all pages
90 | var current_page = active_routes[page].map(function (rule_set) {
91 | var args = {}, scalars = rule_set.rules.scalars, keyvals = rule_set.rules.keyvals, method,
92 | // populate the current request object as a collection of keys/values and scalars
93 | request = path.replace(page, '').split(SL).reduce(function (acc, val) {
94 | var split = val.split(EQ), key = split[0], value = split.slice(1).join(EQ);
95 | return !val.length ? acc // discard empty values, separate rest into scalars or keyvals
96 | : (value ? acc.keyvals[key] = value : acc.scalars.push(val)), acc;
97 | }, {keyvals: {}, scalars: []}), star, keyval,
98 | keyval_keys = keyvals.reduce(function (acc, val) {return (acc[val.name] = 0) || acc;}, {}),
99 | required_scalars_length = scalars.filter(function (val) {return val.required;}).length,
100 | required_keyvals = keyvals.filter(function (val) {return val.required;})
101 | .every(function (val) {return request.keyvals[has](val.name);});
102 | // not enough parameters are supplied in the request for this rule
103 | if (required_scalars_length > request.scalars.length || !required_keyvals) return 0;
104 | if (!rule_set.rules.star) { // too many params are only a problem if the rule isn't a wildcard
105 | if (request.scalars.length > scalars.length) return 0; // if too many scalars are supplied
106 | for (keyval in request.keyvals) // if too many keyvals are supplied
107 | if (request.keyvals[has](keyval) && !keyval_keys[has](keyval)) return 0;
108 | }
109 | request.scalars.slice(0, scalars.length) // populate args scalars
110 | .forEach(function (scalar, index) {args[scalars[index].name] = decode(scalar);});
111 | keyvals.forEach(function (keyval) { // populate args keyvals
112 | if (request.keyvals[keyval.name]) args[keyval.name] = decode(request.keyvals[keyval.name]);
113 | delete request.keyvals[keyval.name]; // remove so that * can be constructed
114 | });
115 | if (rule_set.rules.star) { // all unused scalars and keyvals go into the * argument (still encoded)
116 | star = request.scalars.slice(scalars.length, request.scalars.length);
117 | for (keyval in request.keyvals) if (request.keyvals[has](keyval))
118 | star.push([keyval, request.keyvals[keyval]].join(EQ));
119 | args[rule_set.rules.star] = star.join(SL);
120 | }
121 | try { // make sure the rule's method actually exists and can be accessed
122 | method = rule_set.method.split('.').reduce(function (acc, val) {return acc[val];}, context);
123 | if (typeof method !== 'function') throw new Error;
124 | } catch (error) {
125 | throw new TypeError(self + ': ' + rule_set.method + ' is not a function in current context');
126 | }
127 | return {page: page, hash: routes.hash({route: rule_set.raw}, args), method: method, args: args};
128 | });
129 | return acc.concat(current_page).filter(Boolean); // only return the parsed rules that matched
130 | }, []).sort(function (a, b) {return b.hash.length - a.hash.length;}); // order in descending hash length
131 | },
132 | /**
133 | * builds the internal representation of a rule based on the route definition
134 | * @inner
135 | * @name RouteMap#compile
136 | * @function
137 | * @param {String} route
138 | * @throws {SyntaxError} if any portion of a rule definition follows a * directive
139 | * @throws {SyntaxError} if a required scalar follows an optional scalar
140 | * @throws {SyntaxError} if a rule cannot be parsed
141 | * @type {Object}
142 | * @returns {Object} a compiled object, for example, the rule '/foo/:id/type:?/rest:*' would return
143 | * an object of the form:
{
144 | * page:'/foo',
145 | * rules:{
146 | * keyvals:[{name: 'type', required: false}],
147 | * scalars:[{name: 'id', required: true}],
148 | * star:'rest' // false if not defined
149 | * }
150 | * }
151 | * @see RouteMap.add
152 | * @see RouteMap.hash
153 | * @see RouteMap.remove
154 | */
155 | compile = (function (memo) { // compile is slow so cache compiled objects in a memo
156 | return function (orig) {
157 | var self = 'compile', compiled, index, names = {},
158 | route = orig[0] === SL ? orig : ~(index = orig.indexOf(SL)) ? orig.slice(index) : 0,
159 | /** @ignore */
160 | valid_name = function (name) {
161 | if (names[has](name) || (names[name] = 0))
162 | throw new SyntaxError(self + ': "' + name + '" is repeated in: ' + orig);
163 | };
164 | if (!route) throw new SyntaxError(self + ': the route ' + orig + ' was not understood');
165 | if (memo[route]) return memo[route];
166 | compiled = route.split(SL).reduce(function (acc, val) {
167 | var rules = acc.rules, scalars = rules.scalars, keyvals = rules.keyvals;
168 | if (rules.star) throw new SyntaxError(self + ': no rules can follow a * directive in: ' + orig);
169 | // construct the name of the page
170 | if (!~val.search(token_exp) && !scalars.length && !keyvals.length) return acc.page.push(val), acc;
171 | // construct the parameters
172 | if (val.match(star_exp)) return (rules.star = RegExp.$2 || RegExp.$3), valid_name(rules.star), acc;
173 | if (val.match(scalar_exp)) {
174 | if (acc.has_optional_scalar) // no scalars can follow optional scalars
175 | throw new SyntaxError(self + ': "' + val + '" cannot follow an optional rule in: ' + orig);
176 | if (!!RegExp.$2) acc.has_optional_scalar = val;
177 | return scalars.push({name: RegExp.$1, required: !RegExp.$2}), valid_name(RegExp.$1), acc;
178 | }
179 | if (val.match(keyval_exp))
180 | return keyvals.push({name: RegExp.$1, required: !RegExp.$2}), valid_name(RegExp.$1), acc;
181 | throw new SyntaxError(self + ': the rule "' + val + '" was not understood in: ' + orig);
182 | }, {page: [], rules: {scalars: [], keyvals: [], star: false}, has_optional_scalar: ''});
183 | delete compiled.has_optional_scalar; // this is just a temporary value and should not be exposed
184 | compiled.page = compiled.page.join(SL).replace(new RegExp(SL + '$'), '') || SL;
185 | return memo[route] = compiled;
186 | };
187 | })({});
188 | pub[namespace] = (routes) = { // parens around routes to satisfy JSDoc's caprice
189 | /**
190 | * adds a rule to the internal table of routes and methods
191 | * @name RouteMap.add
192 | * @function
193 | * @type undefined
194 | * @param {Object} rule rule specification
195 | * @param {String} rule.route route pattern definition; there are three types of pattern arguments: scalars,
196 | * keyvals, and stars; scalars are individual values in a URL (all URL values are separate by the
197 | * '/' character), keyvals are named values, e.g. 'foo=bar', and star values are wildcards; so for
198 | * example, the following pattern represents all the possible options:
199 | * '/foo/:id/:sub?/attr:/subattr:?/rest:*'
the ? means that argument is
200 | * optional, the star rule is named rest but it could have just simply been left as *,
201 | * which means the resultant dictionary would have put the wildcard remainder into args['*']
202 | * instead of args.rest; so the following URL would match the pattern above:
212 | * add uses {@link #compile} and does not catch any errors thrown by that function
213 | * @param {String} rule.method listener method for this route
214 | * @throws {TypeError} if rule.route or rule.method are not strings or empty strings
215 | * @throws {Error} if rule has already been added
216 | * @see RouteMap.post_add
217 | */
218 | add: function (rule) {
219 | var self = 'add', method = rule.method, route = rule.route, compiled, id = fingerprint(rule);
220 | if ([route, method].some(invalid_str))
221 | throw new TypeError(self + ': rule.route and rule.method must both be non-empty strings');
222 | if (added_routes[id]) throw new Error(self + ': ' + route + ' to ' + method + ' already exists');
223 | compiled = compile(route);
224 | added_routes[id] = true;
225 | if (!active_routes[compiled.page] && (active_routes[compiled.page] = [])) // add route to list and sort
226 | flat_pages = flat_pages.concat(compiled.page).sort(function (a, b) {return b.length - a.length;});
227 | active_routes[compiled.page].push(routes.post_add({method: method, rules: compiled.rules, raw: route}));
228 | },
229 | /**
230 | * overrides the context where listener methods are sought, the default scope is window
231 | * (in a browser setting), returns the current context, if no scope object is passed in, just
232 | * returns current context without setting context
233 | * @name RouteMap.context
234 | * @function
235 | * @type {Object}
236 | * @returns {Object} the current context within which RouteMap searches for handlers
237 | * @param {Object} scope the scope within which methods for mapped routes will be looked for
238 | */
239 | context: function (scope) {return context = typeof scope === 'object' ? scope : context;},
240 | /**
241 | * returns the parsed (see {@link #parse}) currently accessed route; after listeners have finished
242 | * firing, current and last are the same
243 | * @name RouteMap.current
244 | * @function
245 | * @type Object
246 | * @returns {Object} the current parsed URL object
247 | * @see RouteMap.last
248 | */
249 | current: function () {return current ? merge(current) : null;},
250 | /**
251 | * this function is fired when no rule is matched by a URL, by default it does nothing, but it could be set up
252 | * to handle things like 404 responses on the server-side or bad hash fragments in the browser
253 | * @name RouteMap.default_handler
254 | * @function
255 | * @type undefined
256 | */
257 | default_handler: function () {},
258 | /**
259 | * URL grabber function, defaults to checking the URL fragment (hash); this function should be
260 | * overwritten in a server-side environment; this method is called by {@link RouteMap.handler}; without
261 | * window.location.hash it will return '/'
262 | * @name RouteMap.get
263 | * @function
264 | * @returns {String} by default, this returns a subset of the URL hash (everything after the first
265 | * '/' character ... if nothing follows a slash, it returns '/'); if overwritten, it
266 | * must be a function that returns URL path strings (beginning with '/') to match added rules
267 | * @type String
268 | */
269 | get: function () {
270 | if (typeof window === 'undefined') return SL;
271 | var hash = window.location.hash, index = hash.indexOf(SL);
272 | return ~index ? hash.slice(index) : SL;
273 | },
274 | /**
275 | * in a browser setting, it changes window.location.hash, in other settings, it should be
276 | * overwritten to do something useful (if necessary); it will not throw an error if window does
277 | * not exist
278 | * @name RouteMap.go
279 | * @function
280 | * @type undefined
281 | * @param {String} hash the hash fragment to go to
282 | */
283 | go: function (hash) {
284 | if (typeof window !== 'undefined') window.location.hash = (hash.indexOf(PR) === 0 ? '' : PR) + hash;
285 | },
286 | /**
287 | * main handler function for routing, this should be bound to hashchange events in the browser, or
288 | * (in conjunction with updating {@link RouteMap.get}) used with the HTML5 history API, it detects
289 | * all the matching route patterns, parses the URL parameters and fires their methods with the arguments from
290 | * the parsed URL; the timing of {@link RouteMap.current} and {@link RouteMap.last} being set is as follows
291 | * (pseudo-code):
292 | *
301 | * RouteMap.handler calls {@link #parse} and does not catch any errors that function throws
302 | * @name RouteMap.handler
303 | * @function
304 | * @type undefined
305 | * @see RouteMap.pre_dispatch
306 | */
307 | handler: function () {
308 | var url = routes.get(), parsed = parse(url), args = Array.prototype.slice.call(arguments);
309 | if (!parsed.length) return routes.default_handler.apply(null, [url].concat(args));
310 | current = parsed[0]; // set current to the longest hash before pre_dispatch touches it
311 | parsed = routes.pre_dispatch(parsed); // pre_dispatch might change the contents of parsed
312 | current = parsed[0]; // set current to the longest hash again after pre_dispatch
313 | parsed.forEach(function (val) {val.method.apply(null, [val.args].concat(args));}); // fire requested methods
314 | last = parsed[0];
315 | },
316 | /**
317 | * returns a URL fragment by applying parameters to a rule; uses {@link #compile} and does not catch any errors
318 | * thrown by that function
319 | * @name RouteMap.hash
320 | * @function
321 | * @type String
322 | * @param {Object} rule the rule specification; it typically looks like:
323 | * {route:'/foo', method:'bar'}
but only route is strictly necessary
324 | * @param {Object} params a dictionary of argument key/value pairs required by the rule
325 | * @returns {String} URL fragment resulting from applying arguments to rule pattern
326 | * @throws {TypeError} if a required parameter is not present
327 | */
328 | hash: function (rule, params) {
329 | var self = 'hash', hash, compiled, params = params || {};
330 | if (invalid_str(rule.route)) throw new TypeError(self + ': rule.route must be a non-empty string');
331 | compiled = compile(rule.route);
332 | hash = compiled.page + (compiled.page === SL ? '' : SL) + // 1. start with page, then add params
333 | compiled.rules.scalars.map(function (val) { // 2. add scalar values next
334 | var value = encode(params[val.name]), bad_param = params[val.name] === void 0 || invalid_str(value);
335 | if (val.required && bad_param)
336 | throw new TypeError(self + ': params.' + val.name + ' is undefined, route: ' + rule.route);
337 | return bad_param ? 0 : value;
338 | })
339 | .concat(compiled.rules.keyvals.map(function (val) { // 3. then concat keyval values
340 | var value = encode(params[val.name]), bad_param = params[val.name] === void 0 || invalid_str(value);
341 | if (val.required && bad_param)
342 | throw new TypeError(self + ': params.' + val.name + ' is undefined, route: ' + rule.route);
343 | return bad_param ? 0 : val.name + EQ + value;
344 | }))
345 | .filter(Boolean).join(SL); // remove empty (0) values
346 | if (compiled.rules.star && params[compiled.rules.star]) // 4. add star value if it exists
347 | hash += (hash[hash.length - 1] === SL ? '' : SL) + params[compiled.rules.star];
348 | return hash;
349 | },
350 | /**
351 | * returns the parsed (see {@link #parse}) last accessed route; when route listeners are being called,
352 | * last is the previously accessed route, after listeners have finished firing, the current parsed
353 | * route replaces last's value
354 | * @name RouteMap.last
355 | * @function
356 | * @type Object
357 | * @returns {Object} the last parsed URL object, will be null on first load
358 | * @see RouteMap.current
359 | */
360 | last: function () {return last ? merge(last) : null;},
361 | /**
362 | * parses a URL fragment into a data structure only if there is a route whose pattern matches the fragment
363 | * @name RouteMap.parse
364 | * @function
365 | * @type Object
366 | * @returns {Object} of the form:
{page:'/foo', args:{bar:'some_value'}}
367 | * only if a rule with the route: '/foo/:bar' has already been added
368 | * @throws {TypeError} if hash is not a string, is empty, or does not contain a '/' character
369 | * @throws {SyntaxError} if hash cannot be parsed by {@link #parse}
370 | */
371 | parse: function (hash) {
372 | var self = 'parse', parsed, index = hash.indexOf(SL);
373 | hash = ~index ? hash.slice(index) : '';
374 | if (invalid_str(hash)) throw new TypeError(self + ': hash must be a string with a ' + SL + ' character');
375 | if (!(parsed = parse(hash)).length) throw new SyntaxError(self + ': ' + hash + ' cannot be parsed');
376 | return {page: parsed[0].page, args: parsed[0].args};
377 | },
378 | /**
379 | * this function is called by {@link RouteMap.add}, it receives a compiled rule object, e.g. for the rule:
380 | *