1 /**
  2  * <p>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.</p>
  4  * <p>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  * <a href="https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/">Mozilla</a> should be
  7  * supplied external to this library.</p>
  8  * <p>It is designed to be used in both a browser setting and a server-side context (for example in node.js).</p>
  9  * <strong>LICENSING INFORMATION:</strong>
 10  * <blockquote><pre>
 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  * </pre></blockquote>
 24  * @see <a href="http://www.opengamma.com/">OpenGamma</a>
 25  * @see <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache License, Version 2.0</a>
 26  * @see <a href="https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/">Mozilla Developer
 27  * Network</a>
 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 <code>*</code> 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 <code>'/foo/:id/type:?/rest:*'</code> would return
143          * an object of the form: <blockquote><pre>{
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          * <code>'/'</code> 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:<blockquote>
199          * <code>'/foo/:id/:sub?/attr:/subattr:?/rest:*'</code></blockquote>the <code>?</code> means that argument is
200          * optional, the star rule is named <code>rest</code> but it could have just simply been left as <code>*</code>,
201          * which means the resultant dictionary would have put the wildcard remainder into <code>args['*']</code>
202          * instead of <code>args.rest</code>; so the following URL would match the pattern above:<blockquote>
203          * <code>/foo/23/45/attr=something/subattr=something_else</code></blockquote>
204          * when its method is called, it will receive this arguments dictionary:<blockquote>
205          * <code><pre>{
206          *      id:'23',
207          *      subid:'45',
208          *      attr:'something',
209          *      subattr:'something_else',
210          *      rest:''
211          * }</pre></code></blockquote>
212          * <code>add</code> 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 <code>rule.route</code> or <code>rule.method</code> are not strings or empty strings
215          * @throws {Error} if <code>rule</code> 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 <code>window</code>
231          * (in a browser setting), returns the current context, if no <code>scope</code> 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, <code>current</code> and <code>last</code> 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 <code>404</code> 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 (<code>hash</code>); this function should be
260          * overwritten in a server-side environment; this method is called by {@link RouteMap.handler}; without
261          * <code>window.location.hash</code> it will return <code>'/'</code>
262          * @name RouteMap.get
263          * @function
264          * @returns {String} by default, this returns a subset of the URL hash (everything after the first
265          * <code>'/'</code> character ... if nothing follows a slash, it returns <code>'/'</code>); if overwritten, it
266          * must be a function that returns URL path strings (beginning with <code>'/'</code>) 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 <code>window.location.hash</code>, in other settings, it should be
276          * overwritten to do something useful (if necessary); it will not throw an error if <code>window</code> 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 <code>hashchange</code> events in the browser, or
288          * (in conjunction with updating {@link RouteMap.get}) used with the HTML5 <code>history</code> 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          * <blockquote><pre>
293          * path: get_route             // {@link RouteMap.get}
294          * parsed: parse path          // {@link #parse}
295          * current: longest parsed     // {@link RouteMap.current}
296          * parsed: pre_dispatch parsed // {@link RouteMap.pre_dispatch}
297          * current: longest parsed     // reset current
298          * fire matched rules in parsed
299          * last: current               // {@link RouteMap.last}
300          * </pre></blockquote>
301          * <code>RouteMap.handler</code> 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: <blockquote>
323          * <code>{route:'/foo', method:'bar'}</code></blockquote> but only <code>route</code> 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          * <code>last</code> is the previously accessed route, after listeners have finished firing, the current parsed
353          * route replaces <code>last</code>'s value
354          * @name RouteMap.last
355          * @function
356          * @type Object
357          * @returns {Object} the last parsed URL object, will be <code>null</code> 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: <blockquote><code>{page:'/foo', args:{bar:'some_value'}}</code></blockquote>
367          * only if a rule with the route: <code>'/foo/:bar'</code> has already been added
368          * @throws {TypeError} if hash is not a string, is empty, or does not contain a <code>'/'</code> 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          * <blockquote><code>{route:'/foo/:id/:sub?/attr:/subattr:?/rest:*', method:'console.log'}</code></blockquote>
381          * <code>post_add</code> would receive the following object:
382          * <blockquote><code><pre>{
383          *     method:'console.log',
384          *     rules:{
385          *         scalars:[{name:'id',required:true},{name:'sub',required:false}],
386          *         keyvals:[{name:'attr',required:true},{name:'subattr',required:false}],
387          *         star:'rest'
388          *     },
389          *     raw:'/foo/:id/:sub?/attr:/subattr:?/rest:*'
390          * }</pre></code></blockquote>
391          * and it is expected to pass back an object of the same format; it can be overwritten to post-process added
392          * rules e.g. to add extra default application-wide parameters; by default, it simply returns what was passed
393          * into it
394          * @name RouteMap.post_add
395          * @function
396          * @type Object
397          * @returns {Object} the default function returns the exact object it received; a custom function needs to
398          * an object that is of the same form (but could possibly have more or fewer parameters, etc.)
399          * @param {Object} compiled the compiled rule
400          */
401         post_add: function (compiled) {return compiled;},
402         /**
403          * like {@link RouteMap.post_add} this function can be overwritten to add application-specific code into
404          * route mapping, it is called before a route begins being dispatched to all matching rules; it receives the
405          * list of matching parsed route objects ({@link #parse}) and is expected to return it; one application of this
406          * function might be to set application-wide variables like debug flags
407          * @name RouteMap.pre_dispatch
408          * @function
409          * @type Array
410          * @returns {Array} a list of the same form as the one it receives
411          * @param {Array} parsed the parsed request
412          */
413         pre_dispatch: function (parsed) {return parsed;},
414         /**
415          * if a string is passed in, it overwrites the prefix that is removed from each URL before parsing; primarily
416          * used for hashbang (<code>#!</code>); either way, it returns the current prefix
417          * @name RouteMap.prefix
418          * @function
419          * @type undefined
420          * @param {String} prefix (optional) the prefix string
421          */
422         prefix: function (prefix) {return PR = typeof prefix !== 'undefined' ? prefix + '' : PR;},
423         /**
424          * counterpart to {@link RouteMap.add}, removes a rule specification; * <code>remove</code> uses
425          * {@link #compile} and does not catch any errors thrown by that function
426          * @name RouteMap.remove
427          * @function
428          * @type undefined
429          * @param {Object} rule the rule specification that was used in {@link RouteMap.add}
430          * @throws {TypeError} if <code>rule.route</code> or <code>rule.method</code> are not strings or empty strings
431          */
432         remove: function (rule) {
433             var self = 'remove', method = rule.method, route = rule.route, compiled, id = fingerprint(rule), index;
434             if ([route, method].some(invalid_str))
435                 throw new TypeError(self + ': rule.route and rule.method must both be non-empty strings');
436             if (!added_routes[id]) return;
437             compiled = compile(route);
438             delete added_routes[id];
439             active_routes[compiled.page] = active_routes[compiled.page]
440                 .filter(function (rule) {return (rule.raw !== route) || (rule.method !== method);});
441             if (!active_routes[compiled.page].length && (delete active_routes[compiled.page])) // delete active route
442                 if (~(index = flat_pages.indexOf(compiled.page))) flat_pages.splice(index, 1); // then flat page
443         }
444     };
445 })(typeof exports === 'undefined' ? window : exports, 'RouteMap');